diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 61a9130cd9669c3843e6445dfe1fee2d493869bc..fb7f4a8a465d42b4a0390d464b83b99e8465bba7 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="CompilerConfiguration"> - <bytecodeTargetLevel target="1.8" /> + <bytecodeTargetLevel target="11" /> </component> </project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index d5d35ec44f10991b508f6454a85204a276726364..860da66a5ea990f8e3c36fcdb0e2e05dacadf880 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> - <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK"> + <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <output url="file://$PROJECT_DIR$/build/classes" /> </component> <component name="ProjectType"> diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml index 7f68460d8b38ac04e3a3224d7c79ef719b1991a9..e497da999824b5c5629039f2e74794dd96b6c419 100644 --- a/.idea/runConfigurations.xml +++ b/.idea/runConfigurations.xml @@ -3,6 +3,7 @@ <component name="RunConfigurationProducerService"> <option name="ignoredProducers"> <set> + <option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" /> <option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" /> <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" /> <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" /> diff --git a/build.gradle b/build.gradle index affee31588a15c069a98159df4f174244dd32f73..22f247b6a7d440171c5033c0e9a34f013be83074 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ buildscript { // Ref: https://kotlinlang.org/releases.html - ext.kotlin_version = '1.4.32' - ext.kotlin_coroutines_version = "1.4.2" + ext.kotlin_version = '1.5.10' + ext.kotlin_coroutines_version = "1.5.0" repositories { google() jcenter() @@ -12,7 +12,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:4.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 517ae0d4cec4df8ed790c8a1c549bab7e37dfa89..e1e2fd2c75e69ac5748b9c767a2b01d8e45f8b30 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=3db89524a3981819ff28c3f979236c1274a726e146ced0c8a2020417f9bc0782 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip +distributionSha256Sum=13bf8d3cf8eeeb5770d19741a59bde9bd966dd78d17f1bbad787a05ef19d1c2d +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 046846e5c140ca94a00f8a0b2f4a851f6d958644..973d09bec877e75eb0ad1a62ce8ab8da6412336f 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -6,13 +6,10 @@ apply plugin: 'realm-android' buildscript { repositories { - // mavenCentral() - //noinspection GrDeprecatedAPIUsage - jcenter() + mavenCentral() } dependencies { - // Stick to this version until https://github.com/realm/realm-java/issues/7402 is fixed - classpath "io.realm:realm-gradle-plugin:10.3.1" + classpath "io.realm:realm-gradle-plugin:10.5.0" } } @@ -24,7 +21,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode 1 - versionName "1.1.5" + versionName "1.1.8" // Multidex is useful for tests multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -115,7 +112,7 @@ dependencies { def lifecycle_version = '2.2.0' def arch_version = '2.1.0' def markwon_version = '3.1.0' - def daggerVersion = '2.33' + def daggerVersion = '2.35.1' def work_version = '2.5.0' def retrofit_version = '2.9.0' @@ -123,7 +120,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" - implementation "androidx.appcompat:appcompat:1.2.0" + implementation "androidx.appcompat:appcompat:1.3.0" implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" @@ -168,8 +165,11 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0' + // Video compression + implementation 'com.otaliastudios:transcoder:0.10.3' + // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.21' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.23' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.5.1' @@ -187,7 +187,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' - androidTestImplementation 'org.amshove.kluent:kluent-android:1.61' + androidTestImplementation 'org.amshove.kluent:kluent-android:1.65' androidTestImplementation 'io.mockk:mockk-android:1.11.0' androidTestImplementation "androidx.arch.core:core-testing:$arch_version" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" 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 5815b23c0642867848b77f76efd6ca65e1f54e81..da176491c696bb8105cfc5f3afe4ee4dabb6b576 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 @@ -66,8 +66,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData { val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) - val roomId = mTestHelper.doSync<String> { - aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it) + val roomId = mTestHelper.runBlockingTest { + aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }) } if (encryptedRoom) { @@ -135,7 +135,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { bobRoomSummariesLive.observeForever(roomJoinedObserver) } - mTestHelper.doSync<Unit> { bobSession.joinRoom(aliceRoomId, callback = it) } + mTestHelper.runBlockingTest { bobSession.joinRoom(aliceRoomId) } mTestHelper.await(lock) @@ -176,8 +176,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { room.invite(samSession.myUserId, null) } - mTestHelper.doSync<Unit> { - samSession.joinRoom(room.roomId, null, emptyList(), it) + mTestHelper.runBlockingTest { + samSession.joinRoom(room.roomId, null, emptyList()) } return samSession @@ -256,8 +256,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { } fun createDM(alice: Session, bob: Session): String { - val roomId = mTestHelper.doSync<String> { - alice.createDirectRoom(bob.myUserId, it) + val roomId = mTestHelper.runBlockingTest { + alice.createDirectRoom(bob.myUserId) } mTestHelper.waitWithLatch { latch -> @@ -300,7 +300,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { bobRoomSummariesLive.observeForever(newRoomObserver) } - mTestHelper.doSync<Unit> { bob.joinRoom(roomId, callback = it) } + mTestHelper.runBlockingTest { bob.joinRoom(roomId) } } return roomId @@ -398,8 +398,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) aliceSession.cryptoService().setWarnOnUnknownDevices(false) - val roomId = mTestHelper.doSync<String> { - aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it) + val roomId = mTestHelper.runBlockingTest { + aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }) } val room = aliceSession.getRoom(roomId)!! @@ -412,7 +412,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { val session = mTestHelper.createAccount("User_$index", defaultSessionParams) mTestHelper.runBlockingTest(timeout = 600_000) { room.invite(session.myUserId, null) } println("TEST -> " + session.myUserId + " invited") - mTestHelper.doSync<Unit> { session.joinRoom(room.roomId, null, emptyList(), it) } + mTestHelper.runBlockingTest { session.joinRoom(room.roomId, null, emptyList()) } println("TEST -> " + session.myUserId + " joined") sessions.add(session) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt index 33de3456309f95d9b399341d4c20ebe905f4900e..1d05e655afd8943ea7ff6f18dfb64fcad7d30a80 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.di.MatrixModule import org.matrix.android.sdk.internal.di.MatrixScope import org.matrix.android.sdk.internal.di.NetworkModule import org.matrix.android.sdk.internal.raw.RawModule +import org.matrix.android.sdk.internal.util.system.SystemModule @Component(modules = [ TestModule::class, @@ -33,6 +34,7 @@ import org.matrix.android.sdk.internal.raw.RawModule NetworkModule::class, AuthModule::class, RawModule::class, + SystemModule::class, TestNetworkModule::class ]) @MatrixScope diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt index 122584142e57fc5c03a6bca2d5eac3452150d287..a2566c14149a3034e0f63bfa5f24a2eab28cc67d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -29,8 +31,6 @@ import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent -import kotlin.test.assertEquals -import kotlin.test.assertNotNull @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -54,7 +54,7 @@ class PreShareKeysTest : InstrumentedTest { && it.getClearType() == EventType.ROOM_KEY } - assertEquals(0, preShareCount, "Bob should not have receive any key from alice at this point") + assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount) Log.d("#Test", "Room Key Received from alice $preShareCount") // Force presharing of new outbound key @@ -78,14 +78,14 @@ class PreShareKeysTest : InstrumentedTest { } val content = latest?.getClearContent().toModel<RoomKeyContent>() - assertNotNull(content, "Bob should have received and decrypted a room key event from alice") - assertEquals(e2eRoomID, content.roomId, "Wrong room") + assertNotNull("Bob should have received and decrypted a room key event from alice", content) + assertEquals("Wrong room", e2eRoomID, content!!.roomId) val megolmSessionId = content.sessionId!! val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId) .getObject(bobSession.myUserId, bobSession.sessionParams.deviceId) - assertEquals(0, sharedIndex, "The session received by bob should match what alice sent") + assertEquals("The session received by bob should match what alice sent", 0, sharedIndex) // Just send a real message as test val sentEvent = mTestHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index e6b364f3fb3d9cf0017963f5ec7b3344c9253114..40659cef11a4a955e886a6eaeefe14c8f6e2724f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -71,13 +71,12 @@ class KeyShareTests : InstrumentedTest { val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) // Create an encrypted room and add a message - val roomId = mTestHelper.doSync<String> { + val roomId = mTestHelper.runBlockingTest { aliceSession.createRoom( CreateRoomParams().apply { visibility = RoomDirectoryVisibility.PRIVATE enableEncryption() - }, - it + } ) } val room = aliceSession.getRoom(roomId) @@ -332,13 +331,12 @@ class KeyShareTests : InstrumentedTest { } // Create an encrypted room and send a couple of messages - val roomId = mTestHelper.doSync<String> { + val roomId = mTestHelper.runBlockingTest { aliceSession.createRoom( CreateRoomParams().apply { visibility = RoomDirectoryVisibility.PRIVATE enableEncryption() - }, - it + } ) } val roomAlicePov = aliceSession.getRoom(roomId) @@ -371,8 +369,8 @@ class KeyShareTests : InstrumentedTest { roomAlicePov.invite(bobSession.myUserId, null) } - mTestHelper.doSync<Unit> { - bobSession.joinRoom(roomAlicePov.roomId, null, emptyList(), it) + mTestHelper.runBlockingTest { + bobSession.joinRoom(roomAlicePov.roomId, null, emptyList()) } // we want to discard alice outbound session diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt index ee604fc9abcc8b03a86509604f8ac03055f23256..76bf6dc0408aa82365e5c840cb195eb46e118536 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -226,12 +226,12 @@ class QrCodeTest : InstrumentedTest { private fun checkHeader(byteArray: ByteArray) { // MATRIX - byteArray[0] shouldBeEqualTo 'M'.toByte() - byteArray[1] shouldBeEqualTo 'A'.toByte() - byteArray[2] shouldBeEqualTo 'T'.toByte() - byteArray[3] shouldBeEqualTo 'R'.toByte() - byteArray[4] shouldBeEqualTo 'I'.toByte() - byteArray[5] shouldBeEqualTo 'X'.toByte() + byteArray[0] shouldBeEqualTo 'M'.code.toByte() + byteArray[1] shouldBeEqualTo 'A'.code.toByte() + byteArray[2] shouldBeEqualTo 'T'.code.toByte() + byteArray[3] shouldBeEqualTo 'R'.code.toByte() + byteArray[4] shouldBeEqualTo 'I'.code.toByte() + byteArray[5] shouldBeEqualTo 'X'.code.toByte() // Version byteArray[6] shouldBeEqualTo 2 diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ee6caed0da5c0859b5ec27d3736e9096d69feba --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.securestorage + +import android.os.Build +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.amshove.kluent.shouldBeEqualTo +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import java.io.ByteArrayOutputStream +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class SecretStoringUtilsTest : InstrumentedTest { + + private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider() + private val secretStoringUtils = SecretStoringUtils(context(), buildVersionSdkIntProvider) + + companion object { + const val TEST_STR = "This is something I want to store safely!" + } + + @Test + fun testStringNominalCaseApi21() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP + // Encrypt + val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + // Decrypt + val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testStringNominalCaseApi23() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.M + // Encrypt + val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + // Decrypt + val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testStringNominalCaseApi30() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.R + // Encrypt + val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + // Decrypt + val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testStringMigration21_23() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP + // Encrypt + val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + + // Simulate a system upgrade + buildVersionSdkIntProvider.value = Build.VERSION_CODES.M + + // Decrypt + val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testObjectNominalCaseApi21() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP + + // Encrypt + val encrypted = ByteArrayOutputStream().also { outputStream -> + outputStream.use { + secretStoringUtils.securelyStoreObject(TEST_STR, alias, it) + } + } + .toByteArray() + .toBase64NoPadding() + // Decrypt + val decrypted = encrypted.fromBase64().inputStream().use { + secretStoringUtils.loadSecureSecret<String>(it, alias) + } + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testObjectNominalCaseApi23() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.M + + // Encrypt + val encrypted = ByteArrayOutputStream().also { outputStream -> + outputStream.use { + secretStoringUtils.securelyStoreObject(TEST_STR, alias, it) + } + } + .toByteArray() + .toBase64NoPadding() + // Decrypt + val decrypted = encrypted.fromBase64().inputStream().use { + secretStoringUtils.loadSecureSecret<String>(it, alias) + } + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testObjectNominalCaseApi30() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.R + + // Encrypt + val encrypted = ByteArrayOutputStream().also { outputStream -> + outputStream.use { + secretStoringUtils.securelyStoreObject(TEST_STR, alias, it) + } + } + .toByteArray() + .toBase64NoPadding() + // Decrypt + val decrypted = encrypted.fromBase64().inputStream().use { + secretStoringUtils.loadSecureSecret<String>(it, alias) + } + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testObjectMigration21_23() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP + + // Encrypt + val encrypted = ByteArrayOutputStream().also { outputStream -> + outputStream.use { + secretStoringUtils.securelyStoreObject(TEST_STR, alias, it) + } + } + .toByteArray() + .toBase64NoPadding() + + // Simulate a system upgrade + buildVersionSdkIntProvider.value = Build.VERSION_CODES.M + + // Decrypt + val decrypted = encrypted.fromBase64().inputStream().use { + secretStoringUtils.loadSecureSecret<String>(it, alias) + } + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + private fun generateAlias() = UUID.randomUUID().toString() +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..b08c88fb246da80217ee2c84e079053e56382319 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.securestorage + +import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider + +class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider { + var value: Int = 0 + + override fun get() = value +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt index ff07cf1d1d4ab473b389cc8e6d2509f818b5dae9..ace48cef77d365a4c03450e226caee540629984f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.session.room.timeline +import org.junit.Assert.fail import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -29,7 +30,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import java.util.concurrent.CountDownLatch -import kotlin.test.fail @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -80,6 +80,7 @@ class TimelineWithManyMembersTest : InstrumentedTest { return@createEventListener true } else { fail("User " + session.myUserId + " decrypted as " + body + " CryptoError: " + it.root.mCryptoError) + false } } ?: return@createEventListener false } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..a1744a0dae2fabf3069df7b54001ab11e1219d78 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt @@ -0,0 +1,222 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.space + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.query.ActiveSpaceFilter +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.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent +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.RoomType +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.space.JoinSpaceResult +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class SpaceCreationTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + @Test + fun createSimplePublicSpace() { + val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true)) + val roomName = "My Space" + val topic = "A public space for test" + var spaceId: String = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + spaceId = session.spaceService().createSpace(roomName, topic, null, true) + // wait a bit to let the summary update it self :/ + it.countDown() + } + } + + val syncedSpace = session.spaceService().getSpace(spaceId) + commonTestHelper.waitWithLatch { + commonTestHelper.retryPeriodicallyWithLatch(it) { + syncedSpace?.asRoom()?.roomSummary()?.name != null + } + } + assertEquals("Room name should be set", roomName, syncedSpace?.asRoom()?.roomSummary()?.name) + assertEquals("Room topic should be set", topic, syncedSpace?.asRoom()?.roomSummary()?.topic) + // assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set") + + assertNotNull("Space should be found by Id", syncedSpace) + val creationEvent = syncedSpace!!.asRoom().getStateEvent(EventType.STATE_ROOM_CREATE) + val createContent = creationEvent?.content.toModel<RoomCreateContent>() + assertEquals("Room type should be space", RoomType.SPACE, createContent?.type) + + var powerLevelsContent: PowerLevelsContent? = null + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val toModel = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)?.content.toModel<PowerLevelsContent>() + powerLevelsContent = toModel + toModel != null + } + } + assertEquals("Space-rooms should be created with a power level for events_default of 100", 100, powerLevelsContent?.eventsDefault) + + val guestAccess = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_GUEST_ACCESS)?.content + ?.toModel<RoomGuestAccessContent>()?.guestAccess + + assertEquals("Public space room should be peekable by guest", GuestAccess.CanJoin, guestAccess) + + val historyVisibility = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY)?.content + ?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility + + assertEquals("Public space room should be world readable", RoomHistoryVisibility.WORLD_READABLE, historyVisibility) + + commonTestHelper.signOutAndClose(session) + } + + @Test + fun testJoinSimplePublicSpace() { + val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true)) + val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true)) + + val roomName = "My Space" + val topic = "A public space for test" + val spaceId: String + runBlocking { + spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true) + // wait a bit to let the summary update it self :/ + delay(400) + } + + // Try to join from bob, it's a public space no need to invite + + val joinResult: JoinSpaceResult + runBlocking { + joinResult = bobSession.spaceService().joinSpace(spaceId) + } + + assertEquals(JoinSpaceResult.Success, joinResult) + + val spaceBobPov = bobSession.spaceService().getSpace(spaceId) + assertEquals("Room name should be set", roomName, spaceBobPov?.asRoom()?.roomSummary()?.name) + assertEquals("Room topic should be set", topic, spaceBobPov?.asRoom()?.roomSummary()?.topic) + + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) + } + + @Test + fun testSimplePublicSpaceWithChildren() { + val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true)) + val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true)) + + val roomName = "My Space" + val topic = "A public space for test" + + val spaceId: String = runBlocking { aliceSession.spaceService().createSpace(roomName, topic, null, true) } + val syncedSpace = aliceSession.spaceService().getSpace(spaceId) + + // create a room + var firstChild: String? = null + commonTestHelper.waitWithLatch { + GlobalScope.launch { + firstChild = aliceSession.createRoom(CreateRoomParams().apply { + this.name = "FirstRoom" + this.topic = "Description of first room" + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + }) + it.countDown() + } + } + + commonTestHelper.waitWithLatch { + GlobalScope.launch { + syncedSpace?.addChildren(firstChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", true, suggested = true) + it.countDown() + } + } + + var secondChild: String? = null + commonTestHelper.waitWithLatch { + GlobalScope.launch { + secondChild = aliceSession.createRoom(CreateRoomParams().apply { + this.name = "SecondRoom" + this.topic = "Description of second room" + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + }) + it.countDown() + } + } + + commonTestHelper.waitWithLatch { + GlobalScope.launch { + syncedSpace?.addChildren(secondChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", false, suggested = true) + it.countDown() + } + } + + // Try to join from bob, it's a public space no need to invite + var joinResult: JoinSpaceResult? = null + commonTestHelper.waitWithLatch { + GlobalScope.launch { + joinResult = bobSession.spaceService().joinSpace(spaceId) + // wait a bit to let the summary update it self :/ + it.countDown() + } + } + + assertEquals(JoinSpaceResult.Success, joinResult) + + val spaceBobPov = bobSession.spaceService().getSpace(spaceId) + assertEquals("Room name should be set", roomName, spaceBobPov?.asRoom()?.roomSummary()?.name) + assertEquals("Room topic should be set", topic, spaceBobPov?.asRoom()?.roomSummary()?.topic) + + // check if bob has joined automatically the first room + + val bobMembershipFirstRoom = bobSession.getRoomSummary(firstChild!!)?.membership + assertEquals("Bob should have joined this room", Membership.JOIN, bobMembershipFirstRoom) + RoomSummaryQueryParams.Builder() + + val childCount = bobSession.getRoomSummaries( + roomSummaryQueryParams { + activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(spaceId) + } + ).size + + assertEquals("Unexpected number of joined children", 1, childCount) + + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..521b5805bd81f116aac86364095ac9fa1d292526 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -0,0 +1,472 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.space + +import android.util.Log +import androidx.lifecycle.Observer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.query.ActiveSpaceFilter +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.RoomType +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.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class SpaceHierarchyTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + @Test + fun createCanonicalChildRelation() { + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) + val spaceName = "My Space" + val topic = "A public space for test" + var spaceId: String = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + spaceId = session.spaceService().createSpace(spaceName, topic, null, true) + it.countDown() + } + } + + val syncedSpace = session.spaceService().getSpace(spaceId) + + var roomId: String = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + roomId = session.createRoom(CreateRoomParams().apply { name = "General" }) + it.countDown() + } + } + + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + commonTestHelper.waitWithLatch { + GlobalScope.launch { + syncedSpace!!.addChildren(roomId, viaServers, null, true) + it.countDown() + } + } + + commonTestHelper.waitWithLatch { + GlobalScope.launch { + session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) + it.countDown() + } + } + + Thread.sleep(9000) + + val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents + val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } + + parents?.forEach { + Log.d("## TEST", "parent : $it") + } + + assertNotNull(parents) + assertEquals(1, parents!!.size) + assertEquals(spaceName, parents.first().roomSummary?.name) + + assertNotNull(canonicalParents) + assertEquals(1, canonicalParents!!.size) + assertEquals(spaceName, canonicalParents.first().roomSummary?.name) + } + +// @Test +// fun testCreateChildRelations() { +// val session = commonTestHelper.createAccount("Jhon", SessionTestParams(true)) +// val spaceName = "My Space" +// val topic = "A public space for test" +// Log.d("## TEST", "Before") +// +// var spaceId = "" +// commonTestHelper.waitWithLatch { +// GlobalScope.launch { +// spaceId = session.spaceService().createSpace(spaceName, topic, null, true) +// it.countDown() +// } +// } +// +// Log.d("## TEST", "created space $spaceId ${Thread.currentThread()}") +// val syncedSpace = session.spaceService().getSpace(spaceId) +// +// val children = listOf("General" to true /*canonical*/, "Random" to false) +// +// // val roomIdList = children.map { +// // runBlocking { +// // session.createRoom(CreateRoomParams().apply { name = it.first }) +// // } to it.second +// // } +// val roomIdList = mutableListOf<Pair<String, Boolean>>() +// commonTestHelper.waitWithLatch { +// GlobalScope.launch { +// children.forEach { +// val rID = session.createRoom(CreateRoomParams().apply { name = it.first }) +// roomIdList.add(rID to it.second) +// } +// it.countDown() +// } +// } +// +// val viaServers = listOf(session.sessionParams.homeServerHost ?: "") +// +// commonTestHelper.waitWithLatch { +// GlobalScope.launch { +// roomIdList.forEach { entry -> +// syncedSpace!!.addChildren(entry.first, viaServers, null, true) +// } +// it.countDown() +// } +// } +// +// commonTestHelper.waitWithLatch { +// GlobalScope.launch { +// roomIdList.forEach { +// session.spaceService().setSpaceParent(it.first, spaceId, it.second, viaServers) +// } +// it.countDown() +// } +// } +// +// roomIdList.forEach { +// val parents = session.getRoom(it.first)?.roomSummary()?.spaceParents +// val canonicalParents = session.getRoom(it.first)?.roomSummary()?.spaceParents?.filter { it.canonical == true } +// +// assertNotNull(parents) +// assertEquals("Unexpected number of parent", 1, parents!!.size) +// assertEquals("Unexpected parent name", spaceName, parents.first().roomSummary?.name) +// assertEquals("Parent of ${it.first} should be canonical ${it.second}", if (it.second) 1 else 0, canonicalParents?.size ?: 0) +// } +// } + + @Test + fun testFilteringBySpace() { + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) + + val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + )) + + val spaceBInfo = createPublicSpace(session, "SpaceB", listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + )) + + val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + )) + + // add C as a subspace of A + val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + commonTestHelper.waitWithLatch { + GlobalScope.launch { + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) + it.countDown() + } + } + + // Create orphan rooms + + var orphan1 = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + orphan1 = session.createRoom(CreateRoomParams().apply { name = "O1" }) + it.countDown() + } + } + + var orphan2 = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + orphan2 = session.createRoom(CreateRoomParams().apply { name = "O2" }) + it.countDown() + } + } + + val allRooms = session.getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) }) + + assertEquals("Unexpected number of rooms", 9, allRooms.size) + + val orphans = session.getFlattenRoomSummaryChildrenOf(null) + + assertEquals("Unexpected number of orphan rooms", 2, orphans.size) + assertTrue("O1 should be an orphan", orphans.any { it.roomId == orphan1 }) + assertTrue("O2 should be an orphan ${orphans.map { it.name }}", orphans.any { it.roomId == orphan2 }) + + val aChildren = session.getFlattenRoomSummaryChildrenOf(spaceAInfo.spaceId) + + assertEquals("Unexpected number of flatten child rooms", 4, aChildren.size) + assertTrue("A1 should be a child of A", aChildren.any { it.name == "A1" }) + assertTrue("A2 should be a child of A", aChildren.any { it.name == "A2" }) + assertTrue("CA should be a grand child of A", aChildren.any { it.name == "C1" }) + assertTrue("A1 should be a grand child of A", aChildren.any { it.name == "C2" }) + + // Add a non canonical child and check that it does not appear as orphan + commonTestHelper.waitWithLatch { + GlobalScope.launch { + val a3 = session.createRoom(CreateRoomParams().apply { name = "A3" }) + spaceA!!.addChildren(a3, viaServers, null, false) + it.countDown() + } + } + + Thread.sleep(2_000) + val orphansUpdate = session.getRoomSummaries(roomSummaryQueryParams { + activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null) + }) + assertEquals("Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}", 2, orphansUpdate.size) + } + + @Test + fun testBreakCycle() { + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) + + val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + )) + + val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + )) + + // add C as a subspace of A + val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + commonTestHelper.waitWithLatch { + GlobalScope.launch { + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) + it.countDown() + } + } + + // add back A as subspace of C + commonTestHelper.waitWithLatch { + GlobalScope.launch { + val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) + spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) + it.countDown() + } + } + + Thread.sleep(1000) + + // A -> C -> A + + val aChildren = session.getFlattenRoomSummaryChildrenOf(spaceAInfo.spaceId) + + assertEquals("Unexpected number of flatten child rooms ${aChildren.map { it.name }}", 4, aChildren.size) + assertTrue("A1 should be a child of A", aChildren.any { it.name == "A1" }) + assertTrue("A2 should be a child of A", aChildren.any { it.name == "A2" }) + assertTrue("CA should be a grand child of A", aChildren.any { it.name == "C1" }) + assertTrue("A1 should be a grand child of A", aChildren.any { it.name == "C2" }) + } + + @Test + fun testLiveFlatChildren() { + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) + + val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + )) + + val spaceBInfo = createPublicSpace(session, "SpaceB", listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + )) + + // add B as a subspace of A + val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + runBlocking { + spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) + } + + val flatAChildren = runBlocking(Dispatchers.Main) { + session.getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) + } + + commonTestHelper.waitWithLatch { latch -> + + val childObserver = object : Observer<List<RoomSummary>> { + override fun onChanged(children: List<RoomSummary>?) { +// Log.d("## TEST", "Space A flat children update : ${children?.map { it.name }}") + System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") + if (children?.any { it.name == "C1" } == true && children.any { it.name == "C2" }) { + // B1 has been added live! + latch.countDown() + flatAChildren.removeObserver(this) + } + } + } + + val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + )) + + // add C as subspace of B + runBlocking { + val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) + spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + } + + // C1 and C2 should be in flatten child of A now + + GlobalScope.launch(Dispatchers.Main) { flatAChildren.observeForever(childObserver) } + } + + // Test part one of the rooms + + val bRoomId = spaceBInfo.roomIds.first() + val bRoom = session.getRoom(bRoomId) + + commonTestHelper.waitWithLatch { latch -> + + val childObserver = object : Observer<List<RoomSummary>> { + override fun onChanged(children: List<RoomSummary>?) { + System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") + if (children?.any { it.roomId == bRoomId } == false) { + // B1 has been added live! + latch.countDown() + flatAChildren.removeObserver(this) + } + } + } + + // part from b room + runBlocking { + bRoom!!.leave(null) + } + // The room should have disapear from flat children + GlobalScope.launch(Dispatchers.Main) { flatAChildren.observeForever(childObserver) } + } + } + + data class TestSpaceCreationResult( + val spaceId: String, + val roomIds: List<String> + ) + + private fun createPublicSpace(session: Session, + spaceName: String, + childInfo: List<Triple<String, Boolean, Boolean?>> + /** Name, auto-join, canonical*/ + ): TestSpaceCreationResult { + var spaceId = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) + it.countDown() + } + } + + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + val roomIds = + childInfo.map { entry -> + var roomId = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + roomId = session.createRoom(CreateRoomParams().apply { name = entry.first }) + it.countDown() + } + } + roomId + } + + roomIds.forEachIndexed { index, roomId -> + runBlocking { + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) + } + } + } + return TestSpaceCreationResult(spaceId, roomIds) + } + + @Test + fun testRootSpaces() { + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) + + val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + )) + + val spaceBInfo = createPublicSpace(session, "SpaceB", listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + )) + + val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + )) + + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + // add C as subspace of B + runBlocking { + val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) + spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + } + + Thread.sleep(2000) + // + A + // a1, a2 + // + B + // b1, b2, b3 + // + C + // + c1, c2 + + val rootSpaces = session.spaceService().getRootSpaceSummaries() + + assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index a7f5163774517dbe0ff588741568028f171a65e9..5e359172431564f5c880c20a9e480f2d50604674 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -51,11 +51,15 @@ interface AuthenticationService { /** * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first. + * + * See [LoginWizard] for more details */ fun getLoginWizard(): LoginWizard /** * Return a RegistrationWizard, to create an matrix account on the homeserver. The login flow has to be retrieved first. + * + * See [RegistrationWizard] for more details. */ fun getRegistrationWizard(): RegistrationWizard diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt index f1f9ba3916b6e5718309a179068d01629230b50e..7d1407c0d81a9186c7cb912ef0a21d251611f316 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt @@ -16,12 +16,10 @@ package org.matrix.android.sdk.api.auth.data -sealed class LoginFlowResult { - data class Success( - val supportedLoginTypes: List<String>, - val ssoIdentityProviders: List<SsoIdentityProvider>?, - val isLoginAndRegistrationSupported: Boolean, - val homeServerUrl: String, - val isOutdatedHomeserver: Boolean - ) : LoginFlowResult() -} +data class LoginFlowResult( + val supportedLoginTypes: List<String>, + val ssoIdentityProviders: List<SsoIdentityProvider>?, + val isLoginAndRegistrationSupported: Boolean, + val homeServerUrl: String, + val isOutdatedHomeserver: Boolean +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt index cfaf74ce240f4ee8c9bf4650b9302815a92a641c..64b3e180aac660ca16dbf6679f69b227d10988dd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt @@ -48,7 +48,7 @@ data class SsoIdentityProvider( */ @Json(name = "brand") val brand: String? -) : Parcelable { +) : Parcelable, Comparable<SsoIdentityProvider> { companion object { const val BRAND_GOOGLE = "org.matrix.google" @@ -58,4 +58,25 @@ data class SsoIdentityProvider( const val BRAND_TWITTER = "org.matrix.twitter" const val BRAND_GITLAB = "org.matrix.gitlab" } + + override fun compareTo(other: SsoIdentityProvider): Int { + return other.toPriority().compareTo(toPriority()) + } + + private fun toPriority(): Int { + return when (brand) { + // We are on Android, so user is more likely to have a Google account + BRAND_GOOGLE -> 5 + // Facebook is also an important SSO provider + BRAND_FACEBOOK -> 4 + // Twitter is more for professionals + BRAND_TWITTER -> 3 + // Here it's very for techie people + BRAND_GITHUB, + BRAND_GITLAB -> 2 + // And finally, if the account has been created with an iPhone... + BRAND_APPLE -> 1 + else -> 0 + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginProfileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginProfileInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..288a6d12326a0aef323d6ed9f79e0bf6016a5c25 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginProfileInfo.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.login + +data class LoginProfileInfo( + val matrixId: String, + val displayName: String?, + val fullAvatarUrl: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt index 9c96cba40c7710ef6edf79ec60e7ba2763ed0f41..a2a93738370e8a79737e3a741f8b9eda48413b05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt @@ -17,34 +17,51 @@ package org.matrix.android.sdk.api.auth.login import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.util.Cancelable +/** + * Set of methods to be able to login to an existing account on a homeserver. + * + * More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signin.md + */ interface LoginWizard { + /** + * Get some information about a matrixId: displayName and avatar url + */ + suspend fun getProfileInfo(matrixId: String): LoginProfileInfo /** - * @param login the login field - * @param password the password field + * Login to the homeserver. + * + * @param login the login field. Can be a user name, or a msisdn (email or phone number) associated to the account + * @param password the password of the account * @param deviceName the initial device name - * @param callback the matrix callback on which you'll receive the result of authentication. - * @return a [Cancelable] + * @return a [Session] if the login is successful */ suspend fun login(login: String, password: String, deviceName: String): Session /** - * Exchange a login token to an access token + * Exchange a login token to an access token. + * + * @param loginToken login token, obtain when login has happen in a WebView, using SSO + * @return a [Session] if the login is successful */ suspend fun loginWithToken(loginToken: String): Session /** - * Reset user password + * Ask the homeserver to reset the user password. The password will not be reset until + * [resetPasswordMailConfirmed] is successfully called. + * + * @param email an email previously associated to the account the user wants the password to be reset. + * @param newPassword the desired new password */ suspend fun resetPassword(email: String, newPassword: String) /** * Confirm the new password, once the user has checked their email + * When this method succeed, tha account password will be effectively modified. */ suspend fun resetPasswordMailConfirmed() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt index 38a5a77291c1e8688b0cd6981ea54cda8350915f..621253faa5c7d31692024c32602591777f8edd0b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt @@ -16,30 +16,98 @@ package org.matrix.android.sdk.api.auth.registration +/** + * Set of methods to be able to create an account on a homeserver. + * + * Common scenario to register an account successfully: + * - Call [getRegistrationFlow] to check that you application supports all the mandatory registration stages + * - Call [createAccount] to start the account creation + * - Fulfill all mandatory stages using the methods [performReCaptcha] [acceptTerms] [dummy], etc. + * + * More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signup.md + * and https://matrix.org/docs/spec/client_server/latest#account-registration-and-management + */ interface RegistrationWizard { - + /** + * Call this method to get the possible registration flow of the current homeserver. + * It can be useful to ensure that your application implementation supports all the stages + * required to create an account. If it is not the case, you will have to use the web fallback + * to let the user create an account with your application. + * See [org.matrix.android.sdk.api.auth.AuthenticationService.getFallbackUrl] + */ suspend fun getRegistrationFlow(): RegistrationResult - suspend fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?): RegistrationResult + /** + * Can be call to check is the desired userName is available for registration on the current homeserver. + * It may also fails if the desired userName is not correctly formatted or does not follow any restriction on + * the homeserver. Ex: userName with only digits may be rejected. + * @param userName the desired username. Ex: "alice" + */ + suspend fun registrationAvailable(userName: String): RegistrationAvailability + + /** + * This is the first method to call in order to create an account and start the registration process. + * + * @param userName the desired username. Ex: "alice" + * @param password the desired password + * @param initialDeviceDisplayName the device display name + */ + suspend fun createAccount(userName: String?, + password: String?, + initialDeviceDisplayName: String?): RegistrationResult + /** + * Perform the "m.login.recaptcha" stage. + * + * @param response the response from ReCaptcha + */ suspend fun performReCaptcha(response: String): RegistrationResult + /** + * Perform the "m.login.terms" stage. + */ suspend fun acceptTerms(): RegistrationResult + /** + * Perform the "m.login.dummy" stage. + */ suspend fun dummy(): RegistrationResult + /** + * Perform the "m.login.email.identity" or "m.login.msisdn" stage. + * + * @param threePid the threePid to add to the account. If this is an email, the homeserver will send an email + * to validate it. For a msisdn a SMS will be sent. + */ suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult + /** + * Ask the homeserver to send again the current threePid (email or msisdn). + */ suspend fun sendAgainThreePid(): RegistrationResult + /** + * Send the code received by SMS to validate a msisdn. + * If the code is correct, the registration request will be executed to validate the msisdn. + */ suspend fun handleValidateThreePid(code: String): RegistrationResult + /** + * Useful to poll the homeserver when waiting for the email to be validated by the user. + * Once the email is validated, this method will return successfully. + * @param delayMillis the SDK can wait before sending the request + */ suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult - suspend fun registrationAvailable(userName: String): RegistrationAvailability - + /** + * This is the current ThreePid, waiting for validation. The SDK will store it in database, so it can be + * restored even if the app has been killed during the registration + */ val currentThreePid: String? - // True when login and password has been sent with success to the homeserver + /** + * True when login and password have been sent with success to the homeserver, i.e. [createAccount] has been + * called successfully. + */ val isRegistrationStarted: Boolean } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt index b2419033649c36684ff79cd54bff16dcd71a08f3..8f1bbb69415304e017729891c44f093d17608087 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt @@ -32,7 +32,6 @@ import java.io.IOException */ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { data class Unknown(val throwable: Throwable? = null) : Failure(throwable) - data class Cancelled(val throwable: Throwable? = null) : Failure(throwable) data class UnrecognizedCertificateFailure(val url: String, val fingerprint: Fingerprint) : Failure() data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException) data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString())) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt index 3820a442aaa7ef42416a7c7301539e39eebf6f50..73b0fe0a7c0604310398706bd243ed166b3c4c17 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt @@ -41,7 +41,7 @@ data class MatrixError( // For M_LIMIT_EXCEEDED @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null, // For M_UNKNOWN_TOKEN - @Json(name = "soft_logout") val isSoftLogout: Boolean = false, + @Json(name = "soft_logout") val isSoftLogout: Boolean? = null, // For M_INVALID_PEPPER // {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"} @Json(name = "lookup_pepper") val newLookupPepper: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/ActiveSpaceFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/ActiveSpaceFilter.kt new file mode 100644 index 0000000000000000000000000000000000000000..48619b9394bb85ba20700c3a9f63147ee750c64f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/ActiveSpaceFilter.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.query + +sealed class ActiveSpaceFilter { + object None : ActiveSpaceFilter() + data class ActiveSpace(val currentSpaceId: String?) : ActiveSpaceFilter() + data class ExcludeSpace(val spaceId: String) : ActiveSpaceFilter() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index a15799d862ff4a601deb57ea1fb0df2e4e291228..b5f90e87ea92a4184e3f774fe8150442c7819b5f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.identity.IdentityService import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -48,6 +49,7 @@ import org.matrix.android.sdk.api.session.search.SearchService 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.signout.SignOutService +import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.terms.TermsService @@ -227,6 +229,16 @@ interface Session : */ fun thirdPartyService(): ThirdPartyService + /** + * Returns the space service associated with the session + */ + fun spaceService(): SpaceService + + /** + * Returns the open id service associated with the session + */ + fun openIdService(): OpenIdService + /** * Add a listener to the session. * @param listener the listener to add. @@ -249,13 +261,13 @@ interface Session : /** * A global session listener to get notified for some events. */ - interface Listener { + interface Listener : SessionLifecycleObserver { /** * Possible cases: * - The access token is not valid anymore, * - a M_CONSENT_NOT_GIVEN error has been received from the homeserver */ - fun onGlobalError(globalError: GlobalError) + fun onGlobalError(session: Session, globalError: GlobalError) } val sharedSecretStorageService: SharedSecretStorageService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionLifecycleObserver.kt similarity index 80% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionLifecycleObserver.kt index cb37fbec75054ad2eff7be0f5ccf8585034865db..b76e454e4b528f9b02b5a7388d759914d3279d49 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionLifecycleObserver.kt @@ -14,20 +14,19 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session +package org.matrix.android.sdk.api.session import androidx.annotation.MainThread /** * This defines methods associated with some lifecycle events of a session. - * A list of SessionLifecycle will be injected into [DefaultSession] */ -internal interface SessionLifecycleObserver { +interface SessionLifecycleObserver { /* Called when the session is opened */ @MainThread - fun onSessionStarted() { + fun onSessionStarted(session: Session) { // noop } @@ -35,7 +34,7 @@ internal interface SessionLifecycleObserver { Called when the session is cleared */ @MainThread - fun onClearCache() { + fun onClearCache(session: Session) { // noop } @@ -43,7 +42,7 @@ internal interface SessionLifecycleObserver { Called when the session is closed */ @MainThread - fun onSessionStopped() { + fun onSessionStopped(session: Session) { // noop } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt index 924da6c19bc1d67851e8cb299da027f640459ed5..ec63eb0be2e242cc0a0e1b830593fc1de0197868 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt @@ -31,6 +31,8 @@ interface ContentUploadStateTracker { sealed class State { object Idle : State() object EncryptingThumbnail : State() + object CompressingImage : State() + data class CompressingVideo(val percent: Float) : State() data class UploadingThumbnail(val current: Long, val total: Long) : State() data class Encrypting(val current: Long, val total: Long) : State() data class Uploading(val current: Long, val total: Long) : State() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 89b873febbb6d593d93fe470acf11a7e1a2b4ee0..6400dd644408f59b01bd2858bb0f4613e485fe57 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -28,6 +28,8 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.di.MoshiProvider import org.json.JSONObject +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.failure.MatrixError import timber.log.Timber typealias Content = JsonDict @@ -90,6 +92,16 @@ data class Event( @Transient var sendState: SendState = SendState.UNKNOWN + @Transient + var sendStateDetails: String? = null + + fun sendStateError(): MatrixError? { + return sendStateDetails?.let { + val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java) + tryOrNull { matrixErrorAdapter.fromJson(it) } + } + } + /** * The `age` value transcoded in a timestamp based on the device clock when the SDK received * the event from the home server. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 905e18b8e8b95cef884fca97ba468b14454e900f..d2befca1ee146e5a1c239320f9dcb875a30589c3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -52,6 +52,10 @@ object EventType { const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access" const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" + const val STATE_SPACE_CHILD = "m.space.child" + + const val STATE_SPACE_PARENT = "m.space.parent" + /** * Note that this Event has been deprecated, see * - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events @@ -74,6 +78,7 @@ object EventType { const val CALL_NEGOTIATE = "m.call.negotiate" const val CALL_REJECT = "m.call.reject" const val CALL_HANGUP = "m.call.hangup" + // This type is not processed by the client, just sent to the server const val CALL_REPLACES = "m.call.replaces" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt index adfdc2498e40c45e4b05d2e1ce0f86224b4ca97d..23dc1e0ba8906a8596e8d5172bce5b3d7cdacebe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -29,14 +29,19 @@ import java.io.File */ interface FileService { - enum class FileState { - IN_CACHE, - DOWNLOADING, - UNKNOWN + sealed class FileState { + /** + * The original file is in cache, but the decrypted files can be deleted for security reason. + * To decrypt the file again, call [downloadFile], the encrypted file will not be downloaded again + * @param decryptedFileInCache true if the decrypted file is available. Always true for clear files. + */ + data class InCache(val decryptedFileInCache: Boolean) : FileState() + object Downloading : FileState() + object Unknown : FileState() } /** - * Download a file. + * Download a file if necessary and ensure that if the file is encrypted, the file is decrypted. * Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision. */ suspend fun downloadFile(fileName: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt new file mode 100644 index 0000000000000000000000000000000000000000..65f6214f93345feff02d7a1abcc8a3617cd8d758 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.openid + +interface OpenIdService { + + /** + * Gets an OpenID token object that the requester may supply to another service to verify their identity in Matrix. + * The generated token is only valid for exchanging for user information from the federation API for OpenID. + */ + suspend fun getOpenIdToken(): OpenIdToken +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdToken.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdToken.kt new file mode 100644 index 0000000000000000000000000000000000000000..2c2ea6568171f225b0a9696e1dd1b49c77672a2d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdToken.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.openid + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class OpenIdToken( + /** + * Required. An access token the consumer may use to verify the identity of the person who generated the token. + * This is given to the federation API GET /openid/userinfo to verify the user's identity. + */ + @Json(name = "access_token") + val accessToken: String, + + /** + * Required. The string "Bearer". + */ + @Json(name = "token_type") + val tokenType: String, + + /** + * Required. The homeserver domain the consumer should use when attempting to verify the user's identity. + */ + @Json(name = "matrix_server_name") + val matrixServerName: String, + + /** + * Required. The number of seconds before this token expires and a new one must be generated. + */ + @Json(name = "expires_in") + val expiresIn: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt index e493adeaf273ad5cb4421a659d48de1764094e5e..05fa24946a3fe21f61cfb993f5662e9df48ea29d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt @@ -66,6 +66,7 @@ interface ProfileService { /** * Get the combined profile information for this user. * This may return keys which are not limited to displayname or avatar_url. + * If server is configured as limit_profile_requests_to_users_who_share_rooms: true then response can be HTTP 403. * @param userId the userId param to look for * */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt index 9ea820f5b3d80280d19833976e01dbbf32e09d2c..a5ec100f64cf29bfd6dc19889d2362deb589e98d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt @@ -66,12 +66,13 @@ interface PushersService { /** * Directly ask the push gateway to send a push to this device + * If successful, the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId. + * In case of error, PusherRejected will be thrown. In this case it means that the pushkey is not valid. + * * @param url the push gateway url (full path) * @param appId the application id * @param pushkey the FCM token * @param eventId the eventId which will be sent in the Push message. Use a fake eventId. - * @param callback callback to know if the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId. - * In case of error, PusherRejected failure can happen. In this case it means that the pushkey is not valid. */ suspend fun testPush(url: String, appId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 257c83564e0c2df663e485b5947a9899b4cbcd5d..8c434fc44016246de6cf3c867a226be9d8ad6bbd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService import org.matrix.android.sdk.api.session.search.SearchResult +import org.matrix.android.sdk.api.session.space.Space import org.matrix.android.sdk.api.util.Optional /** @@ -82,7 +83,7 @@ interface Room : * @param beforeLimit how many events before the result are returned. * @param afterLimit how many events after the result are returned. * @param includeProfile requests that the server returns the historic profile information for the users that sent the events that were returned. - * @param callback Callback to get the search result + * @return The search result */ suspend fun search(searchTerm: String, nextBatch: String?, @@ -91,4 +92,9 @@ interface Room : beforeLimit: Int, afterLimit: Int, includeProfile: Boolean): SearchResult + + /** + * Use this room as a Space, if the type is correct. + */ + fun asSpace(): Space? } 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 8c833644ee51af5b04e541c905855018d5e5ee11..871c5378a6c19fcbbc76f3fdc403e55441d984d5 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 @@ -18,15 +18,14 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData import androidx.paging.PagedList -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary 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.peeking.PeekResult import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription @@ -38,22 +37,19 @@ interface RoomService { /** * Create a room asynchronously */ - fun createRoom(createRoomParams: CreateRoomParams, - callback: MatrixCallback<String>): Cancelable + suspend fun createRoom(createRoomParams: CreateRoomParams): String /** * 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<String>): Cancelable { + suspend fun createDirectRoom(otherUserId: String): String { return createRoom( CreateRoomParams() .apply { invitedUserIds.add(otherUserId) setDirectMessage() enableEncryptionIfInvitedUsersSupportIt = true - }, - callback + } ) } @@ -63,10 +59,9 @@ interface RoomService { * @param reason optional reason for joining the room * @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room. */ - fun joinRoom(roomIdOrAlias: String, - reason: String? = null, - viaServers: List<String> = emptyList(), - callback: MatrixCallback<Unit>): Cancelable + suspend fun joinRoom(roomIdOrAlias: String, + reason: String? = null, + viaServers: List<String> = emptyList()) /** * Get a room from a roomId @@ -112,20 +107,18 @@ interface RoomService { * Inform the Matrix SDK that a room is displayed. * The SDK will update the breadcrumbs in the user account data */ - fun onRoomDisplayed(roomId: String): Cancelable + suspend fun onRoomDisplayed(roomId: String) /** * Mark all rooms as read */ - fun markAllAsRead(roomIds: List<String>, - callback: MatrixCallback<Unit>): Cancelable + suspend fun markAllAsRead(roomIds: List<String>) /** * Resolve a room alias to a room ID. */ - fun getRoomIdByAlias(roomAlias: String, - searchOnServer: Boolean, - callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable + suspend fun getRoomIdByAlias(roomAlias: String, + searchOnServer: Boolean): Optional<RoomAliasDescription> /** * Delete a room alias @@ -172,26 +165,28 @@ interface RoomService { /** * Get some state events about a room */ - fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>) + suspend fun getRoomState(roomId: String): List<Event> /** * Use this if you want to get information from a room that you are not yet in (or invited) * It might be possible to get some information on this room if it is public or if guest access is allowed * This call will try to gather some information on this room, but it could fail and get nothing more */ - fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>) + suspend fun peekRoom(roomIdOrAlias: String): PeekResult /** * TODO Doc */ fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, - pagedListConfig: PagedList.Config = defaultPagedListConfig): LiveData<PagedList<RoomSummary>> + pagedListConfig: PagedList.Config = defaultPagedListConfig, + sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): LiveData<PagedList<RoomSummary>> /** * TODO Doc */ fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, - pagedListConfig: PagedList.Config = defaultPagedListConfig): UpdatableFilterLivePageResult + pagedListConfig: PagedList.Config = defaultPagedListConfig, + sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult /** * TODO Doc @@ -205,4 +200,12 @@ interface RoomService { .setEnablePlaceholders(false) .setPrefetchDistance(10) .build() + + fun getFlattenRoomSummaryChildrenOf(spaceId: String?, memberships: List<Membership> = Membership.activeMemberships()) : List<RoomSummary> + + /** + * Returns all the children of this space, as LiveData + */ + fun getFlattenRoomSummaryChildrenOfLive(spaceId: String?, + memberships: List<Membership> = Membership.activeMemberships()): LiveData<List<RoomSummary>> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt new file mode 100644 index 0000000000000000000000000000000000000000..36da24252733ced7607170ef3f5e7b43020bb433 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room + +enum class RoomSortOrder { + NAME, + ACTIVITY, + NONE +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt index 7e04ebb5f295fbed1ec39a22f444a63fdae81fc7..88ec2de768a4774e7ac19b8ae7de9d8db34eb067 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt @@ -16,15 +16,35 @@ package org.matrix.android.sdk.api.session.room +import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomTagQueryFilter import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { return RoomSummaryQueryParams.Builder().apply(init).build() } +fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): SpaceSummaryQueryParams { + return RoomSummaryQueryParams.Builder() + .apply(init) + .apply { + includeType = listOf(RoomType.SPACE) + excludeType = null + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + } + .build() +} + +enum class RoomCategoryFilter { + ONLY_DM, + ONLY_ROOMS, + ALL +} + /** * This class can be used to filter room summaries to use with: * [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] @@ -35,7 +55,11 @@ data class RoomSummaryQueryParams( val canonicalAlias: QueryStringValue, val memberships: List<Membership>, val roomCategoryFilter: RoomCategoryFilter?, - val roomTagQueryFilter: RoomTagQueryFilter? + val roomTagQueryFilter: RoomTagQueryFilter?, + val excludeType: List<String?>?, + val includeType: List<String?>?, + val activeSpaceFilter: ActiveSpaceFilter?, + var activeGroupId: String? = null ) { class Builder { @@ -46,6 +70,10 @@ data class RoomSummaryQueryParams( var memberships: List<Membership> = Membership.all() var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL var roomTagQueryFilter: RoomTagQueryFilter? = null + var excludeType: List<String?>? = listOf(RoomType.SPACE) + var includeType: List<String?>? = null + var activeSpaceFilter: ActiveSpaceFilter = ActiveSpaceFilter.None + var activeGroupId: String? = null fun build() = RoomSummaryQueryParams( roomId = roomId, @@ -53,7 +81,11 @@ data class RoomSummaryQueryParams( canonicalAlias = canonicalAlias, memberships = memberships, roomCategoryFilter = roomCategoryFilter, - roomTagQueryFilter = roomTagQueryFilter + roomTagQueryFilter = roomTagQueryFilter, + excludeType = excludeType, + includeType = includeType, + activeSpaceFilter = activeSpaceFilter, + activeGroupId = activeGroupId ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt similarity index 72% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt index 71b3c665e709b2bbff8c4b41db0e91f9af016402..b83f57f5ef83ac73626fc8d19118f81eb96eb151 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt @@ -20,8 +20,16 @@ import androidx.lifecycle.LiveData import androidx.paging.PagedList import org.matrix.android.sdk.api.session.room.model.RoomSummary -interface UpdatableFilterLivePageResult { +interface UpdatableLivePageResult { val livePagedList: LiveData<PagedList<RoomSummary>> - fun updateQuery(queryParams: RoomSummaryQueryParams) + fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) + + val liveBoundaries: LiveData<ResultBoundaries> } + +data class ResultBoundaries( + val frontLoaded: Boolean = false, + val endLoaded: Boolean = false, + val zeroItemLoaded: Boolean = false +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt index d2cb7c58a91747e5a3c882365921268945c9d50a..1102eda11c2ffcc89cd61f1da4811a61bf5327d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.api.session.room.alias sealed class RoomAliasError : Throwable() { - object AliasEmpty : RoomAliasError() + object AliasIsBlank : RoomAliasError() object AliasNotAvailable : RoomAliasError() object AliasInvalid : RoomAliasError() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt index 208cdd45563fd120e1d3acd5e689d9c2b0500c5a..deab0ca3e75ee1a76259df29b3277f892406c447 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt @@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.room.alias.RoomAliasError sealed class CreateRoomFailure : Failure.FeatureFailure() { - object CreatedWithTimeout : CreateRoomFailure() + data class CreatedWithTimeout(val roomID: String) : CreateRoomFailure() data class CreatedWithFederationFailure(val matrixError: MatrixError) : CreateRoomFailure() data class AliasError(val aliasError: RoomAliasError) : CreateRoomFailure() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt index e778f5740ddff6e2ab5eae1b546715f75fa8fa22..5c46db7166efbaadd24479c70e3fee3cd3626e40 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt @@ -28,43 +28,43 @@ data class PowerLevelsContent( /** * The level required to ban a user. Defaults to 50 if unspecified. */ - @Json(name = "ban") val ban: Int = Role.Moderator.value, + @Json(name = "ban") val ban: Int? = null, /** * The level required to kick a user. Defaults to 50 if unspecified. */ - @Json(name = "kick") val kick: Int = Role.Moderator.value, + @Json(name = "kick") val kick: Int? = null, /** * The level required to invite a user. Defaults to 50 if unspecified. */ - @Json(name = "invite") val invite: Int = Role.Moderator.value, + @Json(name = "invite") val invite: Int? = null, /** * The level required to redact an event. Defaults to 50 if unspecified. */ - @Json(name = "redact") val redact: Int = Role.Moderator.value, + @Json(name = "redact") val redact: Int? = null, /** * The default level required to send message events. Can be overridden by the events key. Defaults to 0 if unspecified. */ - @Json(name = "events_default") val eventsDefault: Int = Role.Default.value, + @Json(name = "events_default") val eventsDefault: Int? = null, /** * The level required to send specific event types. This is a mapping from event type to power level required. */ - @Json(name = "events") val events: Map<String, Int> = emptyMap(), + @Json(name = "events") val events: Map<String, Int>? = null, /** * The default power level for every user in the room, unless their user_id is mentioned in the users key. Defaults to 0 if unspecified. */ - @Json(name = "users_default") val usersDefault: Int = Role.Default.value, + @Json(name = "users_default") val usersDefault: Int? = null, /** * The power levels for specific users. This is a mapping from user_id to power level for that user. */ - @Json(name = "users") val users: Map<String, Int> = emptyMap(), + @Json(name = "users") val users: Map<String, Int>? = null, /** * The default level required to send state events. Can be overridden by the events key. Defaults to 50 if unspecified. */ - @Json(name = "state_default") val stateDefault: Int = Role.Moderator.value, + @Json(name = "state_default") val stateDefault: Int? = null, /** * The power level requirements for specific notification types. This is a mapping from key to power level for that notifications key. */ - @Json(name = "notifications") val notifications: Map<String, Any> = emptyMap() + @Json(name = "notifications") val notifications: Map<String, Any>? = null ) { /** * Return a copy of this content with a new power level for the specified user @@ -74,7 +74,7 @@ data class PowerLevelsContent( */ fun setUserPowerLevel(userId: String, powerLevel: Int?): PowerLevelsContent { return copy( - users = users.toMutableMap().apply { + users = users.orEmpty().toMutableMap().apply { if (powerLevel == null || powerLevel == usersDefault) { remove(userId) } else { @@ -91,7 +91,7 @@ data class PowerLevelsContent( * @return the level, default to Moderator if the key is not found */ fun notificationLevel(key: String): Int { - return when (val value = notifications[key]) { + return when (val value = notifications.orEmpty()[key]) { // the first implementation was a string value is String -> value.toInt() is Double -> value.toInt() @@ -107,3 +107,12 @@ data class PowerLevelsContent( const val NOTIFICATIONS_ROOM_KEY = "room" } } + +// Fallback to default value, defined in the Matrix specification +fun PowerLevelsContent.banOrDefault() = ban ?: Role.Moderator.value +fun PowerLevelsContent.kickOrDefault() = kick ?: Role.Moderator.value +fun PowerLevelsContent.inviteOrDefault() = invite ?: Role.Moderator.value +fun PowerLevelsContent.redactOrDefault() = redact ?: Role.Moderator.value +fun PowerLevelsContent.eventsDefaultOrDefault() = eventsDefault ?: Role.Default.value +fun PowerLevelsContent.usersDefaultOrDefault() = usersDefault ?: Role.Default.value +fun PowerLevelsContent.stateDefaultOrDefault() = stateDefault ?: Role.Moderator.value diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt index 0760c6f1b42900df128467e6e0795b266b5de284..020e7ed39e50c6c40400bb2025ccdffc19e63de4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt @@ -40,7 +40,7 @@ data class RoomGuestAccessContent( } @JsonClass(generateAdapter = false) -enum class GuestAccess { - @Json(name = "can_join") CanJoin, - @Json(name = "forbidden") Forbidden +enum class GuestAccess(val value: String) { + @Json(name = "can_join") CanJoin("can_join"), + @Json(name = "forbidden") Forbidden("forbidden") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt index f3e8d357f3149f6990ba9a259d0e57a8f34c9da8..a86301a2761dbc5cc6d7c6d1e9540e38579d3be5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt @@ -24,9 +24,10 @@ import com.squareup.moshi.JsonClass * Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules */ @JsonClass(generateAdapter = false) -enum class RoomJoinRules { - @Json(name = "public") PUBLIC, - @Json(name = "invite") INVITE, - @Json(name = "knock") KNOCK, - @Json(name = "private") PRIVATE +enum class RoomJoinRules(val value: String) { + @Json(name = "public") PUBLIC("public"), + @Json(name = "invite") INVITE("invite"), + @Json(name = "knock") KNOCK("knock"), + @Json(name = "private") PRIVATE("private"), + @Json(name = "restricted") RESTRICTED("restricted") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesAllowEntry.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesAllowEntry.kt new file mode 100644 index 0000000000000000000000000000000000000000..7b87bc34d2917c514f9e36286b893fe566955823 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesAllowEntry.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RoomJoinRulesAllowEntry( + /** + * space: The room ID of the space to check the membership of. + */ + @Json(name = "space") val spaceID: String, + /** + * via: A list of servers which may be used to peek for membership of the space. + */ + @Json(name = "via") val via: List<String> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt index 8082486b22f1c8174704eee4c50d715b037e851b..33f402cad3ded1d9ef2a966411332bc7baa71f5d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt @@ -1,5 +1,6 @@ /* * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,14 +27,19 @@ import timber.log.Timber */ @JsonClass(generateAdapter = true) data class RoomJoinRulesContent( - @Json(name = "join_rule") val _joinRules: String? = null + @Json(name = "join_rule") val _joinRules: String? = null, + /** + * If the allow key is an empty list (or not a list at all), then the room reverts to standard public join rules + */ + @Json(name = "allow") val allowList: List<RoomJoinRulesAllowEntry>? = null ) { val joinRules: RoomJoinRules? = when (_joinRules) { - "public" -> RoomJoinRules.PUBLIC - "invite" -> RoomJoinRules.INVITE - "knock" -> RoomJoinRules.KNOCK + "public" -> RoomJoinRules.PUBLIC + "invite" -> RoomJoinRules.INVITE + "knock" -> RoomJoinRules.KNOCK "private" -> RoomJoinRules.PRIVATE - else -> { + "restricted" -> RoomJoinRules.RESTRICTED + else -> { Timber.w("Invalid value for RoomJoinRules: `$_joinRules`") null } 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 9455a83afff2309c5649d8b16391b46d8090c205..cae4775e7173580d369b3564ad5bc60e7cad7427 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 @@ -26,7 +26,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent * This class holds some data of a room. * It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] */ -data class RoomSummary constructor( +data class RoomSummary( val roomId: String, // Computed display name val displayName: String = "", @@ -35,7 +35,9 @@ data class RoomSummary constructor( val avatarUrl: String = "", val canonicalAlias: String? = null, val aliases: List<String> = emptyList(), + val joinRules: RoomJoinRules? = null, val isDirect: Boolean = false, + val directUserId: String? = null, val joinedMembersCount: Int? = 0, val invitedMembersCount: Int? = 0, val latestPreviewableEvent: TimelineEvent? = null, @@ -54,7 +56,11 @@ data class RoomSummary constructor( val inviterId: String? = null, val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null, - val hasFailedSending: Boolean = false + val hasFailedSending: Boolean = false, + val roomType: String? = null, + val spaceParents: List<SpaceParentInfo>? = null, + val spaceChildren: List<SpaceChildInfo>? = null, + val flattenParentIds: List<String> = emptyList() ) { val isVersioned: Boolean @@ -69,6 +75,9 @@ data class RoomSummary constructor( val isFavorite: Boolean get() = hasTag(RoomTag.ROOM_TAG_FAVOURITE) + val isPublic: Boolean + get() = joinRules == RoomJoinRules.PUBLIC + fun hasTag(tag: String) = tags.any { it.name == tag } val canStartCall: Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt index 56503e3e35624a1b1008c2ab88d9c32c56409085..a8a2cfb68b2b51ad4e9b41319d436813186ff241 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt @@ -47,7 +47,7 @@ data class RoomThirdPartyInviteContent( /** * Keys with which the token may be signed. */ - @Json(name = "public_keys") val publicKeys: List<PublicKeys>? = emptyList() + @Json(name = "public_keys") val publicKeys: List<PublicKeys>? ) @JsonClass(generateAdapter = true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomType.kt new file mode 100644 index 0000000000000000000000000000000000000000..b0f3a56d67cccda2da6d744c778b31527088f484 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomType.kt @@ -0,0 +1,22 @@ +/* + * 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.api.session.room.model + +object RoomType { + + const val SPACE = "m.space" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..66293bcb8c6107bf88e4c0b8c0e07b15b7e4e171 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt @@ -0,0 +1,36 @@ +/* + * 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.api.session.room.model + +data class SpaceChildInfo( + val childRoomId: String, + // We might not know this child at all, + // i.e we just know it exists but no info on type/name/etc.. + val isKnown: Boolean, + val roomType: String?, + val name: String?, + val topic: String?, + val avatarUrl: String?, + val order: String?, + val activeMemberCount: Int?, + val autoJoin: Boolean, + val viaServers: List<String>, + val parentRoomId: String?, + val suggested: Boolean?, + val canonicalAlias: String?, + val aliases: List<String>? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceParentInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceParentInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..5ed81b06464a32ddd68901d3cfba5256abf29497 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceParentInfo.kt @@ -0,0 +1,24 @@ +/* + * 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.api.session.room.model + +data class SpaceParentInfo( + val parentId: String?, + val roomSummary: RoomSummary?, + val canonical: Boolean?, + val viaServers: List<String> +) 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 80e3741a0c90e4ed721e4f452ff59807827e2b0b..ca8c66bb3b5b9115a4811c2ab11255e05a139612 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 @@ -18,13 +18,15 @@ 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.GuestAccess import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM // TODO Give a way to include other initial states -class CreateRoomParams { +open class CreateRoomParams { /** * A public visibility indicates that the room will be shown in the published room list. * A private visibility will hide the room from the published room list. @@ -68,6 +70,11 @@ class CreateRoomParams { */ val invite3pids = mutableListOf<ThreePid>() + /** + * Initial Guest Access + */ + var guestAccess: GuestAccess? = null + /** * If set to true, when the room will be created, if cross-signing is enabled and we can get keys for every invited users, * the encryption will be enabled on the created room @@ -111,6 +118,17 @@ class CreateRoomParams { } } + var roomType: String? = null // RoomType.MESSAGING + set(value) { + field = value + if (value != null) { + creationContent[CREATION_CONTENT_KEY_ROOM_TYPE] = value + } else { + // This is the default value, we remove the field + creationContent.remove(CREATION_CONTENT_KEY_ROOM_TYPE) + } + } + /** * The power level content to override in the default power level event */ @@ -136,7 +154,12 @@ class CreateRoomParams { algorithm = MXCRYPTO_ALGORITHM_MEGOLM } + var roomVersion: String? = null + + var joinRuleRestricted: List<RoomJoinRulesAllowEntry>? = null + companion object { private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate" + private const val CREATION_CONTENT_KEY_ROOM_TYPE = "type" } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt index 0b595b1b2b0a399e8985d08e1bcb0bc0f1f5e9bd..52e5c0e9c7f67081934570e0aa22b1fd6b523f3a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt @@ -26,5 +26,7 @@ import com.squareup.moshi.JsonClass data class RoomCreateContent( @Json(name = "creator") val creator: String? = null, @Json(name = "room_version") val roomVersion: String? = null, - @Json(name = "predecessor") val predecessor: Predecessor? = null + @Json(name = "predecessor") val predecessor: Predecessor? = null, + // Defines the room type, see #RoomType (user extensible) + @Json(name = "type") val type: String? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt index e85bb0800a4bb63fb4391b4f7969024776ca2bb3..f21074096eff00850147cd62bfcb10e9bc830585 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt @@ -47,3 +47,10 @@ data class FileInfo( */ @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null ) + +/** + * Get the url of the encrypted thumbnail or of the thumbnail + */ +fun FileInfo.getThumbnailUrl(): String? { + return thumbnailFile?.url ?: thumbnailUrl +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt index 048febec3994329e82b5ad0b27cf1e6fe9b3e67e..c38ef5bc276839509783cd431580b48b38f4d633 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt @@ -40,7 +40,7 @@ data class ImageInfo( /** * Size of the image in bytes. */ - @Json(name = "size") val size: Int = 0, + @Json(name = "size") val size: Long = 0, /** * Metadata about the image referred to in thumbnail_url. @@ -57,3 +57,10 @@ data class ImageInfo( */ @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null ) + +/** + * Get the url of the encrypted thumbnail or of the thumbnail + */ +fun ImageInfo.getThumbnailUrl(): String? { + return thumbnailFile?.url ?: thumbnailUrl +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt index a6908dce5b12a3dab1fa56c128ceb12e6f9ef4ec..a76c3c5b64d4d48f2570747cb28372b8462fcd42 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt @@ -37,3 +37,10 @@ data class LocationInfo( */ @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null ) + +/** + * Get the url of the encrypted thumbnail or of the thumbnail + */ +fun LocationInfo.getThumbnailUrl(): String? { + return thumbnailFile?.url ?: thumbnailUrl +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt index 8379ee933836700a4d00d023d47cf4ab8df7cc42..8a36c26313d1d00ae7a0e2e9b7a4a19ad6e83f3c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt @@ -62,3 +62,10 @@ data class VideoInfo( */ @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null ) + +/** + * Get the url of the encrypted thumbnail or of the thumbnail + */ +fun VideoInfo.getThumbnailUrl(): String? { + return thumbnailFile?.url ?: thumbnailUrl +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt index db70dadef38aca0af96bf92d7c9be164416ad225..888950dc126b118e64debae5c7f6e889a2ba2927 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.room.peeking +import org.matrix.android.sdk.api.util.MatrixItem + sealed class PeekResult { data class Success( val roomId: String, @@ -24,7 +26,9 @@ sealed class PeekResult { val topic: String?, val avatarUrl: String?, val numJoinedMembers: Int?, - val viaServers: List<String> + val roomType: String?, + val viaServers: List<String>, + val someMembers: List<MatrixItem.UserItem>? ) : PeekResult() data class PeekingNotAllowed( @@ -34,4 +38,6 @@ sealed class PeekResult { ) : PeekResult() object UnknownAlias : PeekResult() + + fun isSuccess() = this is Success } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt index 4f1253c6df9d33936267145fef863bb0a36ea9de..99139723a82f15ce874d337f9b667fb653ccc47e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt @@ -18,6 +18,13 @@ package org.matrix.android.sdk.api.session.room.powerlevels import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.banOrDefault +import org.matrix.android.sdk.api.session.room.model.eventsDefaultOrDefault +import org.matrix.android.sdk.api.session.room.model.inviteOrDefault +import org.matrix.android.sdk.api.session.room.model.kickOrDefault +import org.matrix.android.sdk.api.session.room.model.redactOrDefault +import org.matrix.android.sdk.api.session.room.model.stateDefaultOrDefault +import org.matrix.android.sdk.api.session.room.model.usersDefaultOrDefault /** * This class is an helper around PowerLevelsContent. @@ -31,9 +38,9 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { * @return the power level */ fun getUserPowerLevelValue(userId: String): Int { - return powerLevelsContent.users.getOrElse(userId) { - powerLevelsContent.usersDefault - } + return powerLevelsContent.users + ?.get(userId) + ?: powerLevelsContent.usersDefaultOrDefault() } /** @@ -45,7 +52,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { fun getUserRole(userId: String): Role { val value = getUserPowerLevelValue(userId) // I think we should use powerLevelsContent.usersDefault, but Ganfra told me that it was like that on riot-Web - return Role.fromValue(value, powerLevelsContent.eventsDefault) + return Role.fromValue(value, powerLevelsContent.eventsDefaultOrDefault()) } /** @@ -59,11 +66,11 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { fun isUserAllowedToSend(userId: String, isState: Boolean, eventType: String?): Boolean { return if (userId.isNotEmpty()) { val powerLevel = getUserPowerLevelValue(userId) - val minimumPowerLevel = powerLevelsContent.events[eventType] + val minimumPowerLevel = powerLevelsContent.events?.get(eventType) ?: if (isState) { - powerLevelsContent.stateDefault + powerLevelsContent.stateDefaultOrDefault() } else { - powerLevelsContent.eventsDefault + powerLevelsContent.eventsDefaultOrDefault() } powerLevel >= minimumPowerLevel } else false @@ -76,7 +83,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { */ fun isUserAbleToInvite(userId: String): Boolean { val powerLevel = getUserPowerLevelValue(userId) - return powerLevel >= powerLevelsContent.invite + return powerLevel >= powerLevelsContent.inviteOrDefault() } /** @@ -86,7 +93,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { */ fun isUserAbleToBan(userId: String): Boolean { val powerLevel = getUserPowerLevelValue(userId) - return powerLevel >= powerLevelsContent.ban + return powerLevel >= powerLevelsContent.banOrDefault() } /** @@ -96,7 +103,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { */ fun isUserAbleToKick(userId: String): Boolean { val powerLevel = getUserPowerLevelValue(userId) - return powerLevel >= powerLevelsContent.kick + return powerLevel >= powerLevelsContent.kickOrDefault() } /** @@ -106,6 +113,6 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { */ fun isUserAbleToRedact(userId: String): Boolean { val powerLevel = getUserPowerLevelValue(userId) - return powerLevel >= powerLevelsContent.redact + return powerLevel >= powerLevelsContent.redactOrDefault() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt index 066178b1ecfc5f36ea25dbf6f5b876e7455cf584..b3440059e83af932932b764bd7f4c8cb52f86971 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt @@ -20,6 +20,6 @@ data class RoomAggregateNotificationCount( val notificationCount: Int, val highlightCount: Int ) { - val totalCount = notificationCount + highlightCount + val totalCount = notificationCount val isHighlight = highlightCount > 0 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 32f6b94cd8d7a6c903df4e00739e942a2fc1c31e..4a6462477dc653eb91d1c02487494876642602ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.getRelationContent +import org.matrix.android.sdk.api.session.events.model.isEdition import org.matrix.android.sdk.api.session.events.model.isReply import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary @@ -151,6 +152,10 @@ fun TimelineEvent.isReply(): Boolean { return root.isReply() } +fun TimelineEvent.isEdition(): Boolean { + return root.isEdition() +} + fun TimelineEvent.getTextEditableContent(): String? { val lastContent = getLastMessageContent() return if (isReply()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/CreateSpaceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/CreateSpaceParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..42e6584838ce998e4370733e4e64e6d98f031d68 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/CreateSpaceParams.kt @@ -0,0 +1,35 @@ +/* + * 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.api.session.space + +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams + +class CreateSpaceParams : CreateRoomParams() { + + init { + // Space-rooms are distinguished from regular messaging rooms by the m.room.type of m.space + roomType = RoomType.SPACE + + // Space-rooms should be created with a power level for events_default of 100, + // to prevent the rooms accidentally/maliciously clogging up with messages from random members of the space. + powerLevelContentOverride = PowerLevelsContent( + eventsDefault = 100 + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/JoinSpaceResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/JoinSpaceResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..e8c69977c6a59c510a04129e975869f8bffa904f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/JoinSpaceResult.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.space + +sealed class JoinSpaceResult { + object Success : JoinSpaceResult() + data class Fail(val error: Throwable) : JoinSpaceResult() + + /** Success fully joined the space, but failed to join all or some of it's rooms */ + data class PartialSuccess(val failedRooms: Map<String, Throwable>) : JoinSpaceResult() + + fun isSuccess() = this is Success || this is PartialSuccess +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt new file mode 100644 index 0000000000000000000000000000000000000000..db25762c2f2b7436e71a9f25754e1bc1b7b1d22e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.space + +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +interface Space { + + fun asRoom(): Room + + val spaceId: String + + suspend fun leave(reason: String? = null) + + /** + * A current snapshot of [RoomSummary] associated with the space + */ + fun spaceSummary(): RoomSummary? + + suspend fun addChildren(roomId: String, + viaServers: List<String>?, + order: String?, + autoJoin: Boolean = false, + suggested: Boolean? = false) + + suspend fun removeChildren(roomId: String) + + @Throws + suspend fun setChildrenOrder(roomId: String, order: String?) + + @Throws + suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) + + @Throws + suspend fun setChildrenSuggested(roomId: String, suggested: Boolean) + +// fun getChildren() : List<IRoomSummary> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt new file mode 100644 index 0000000000000000000000000000000000000000..fedf38fe065053b30f22f9e0487889aa9595813b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -0,0 +1,87 @@ +/* + * 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.api.session.space + +import android.net.Uri +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult + +typealias SpaceSummaryQueryParams = RoomSummaryQueryParams + +interface SpaceService { + + /** + * Create a space asynchronously + * @return the spaceId of the created space + */ + suspend fun createSpace(params: CreateSpaceParams): String + + /** + * Just a shortcut for space creation for ease of use + */ + suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String + + /** + * Get a space from a roomId + * @param spaceId the roomId to look for. + * @return a space with spaceId or null if room type is not space + */ + fun getSpace(spaceId: String): Space? + + /** + * Try to resolve (peek) rooms and subspace in this space. + * Use this call get preview of children of this space, particularly useful to get a + * preview of rooms that you did not join yet. + */ + suspend fun peekSpace(spaceId: String): SpacePeekResult + + /** + * Get's information of a space by querying the server + */ + suspend fun querySpaceChildren(spaceId: String, + suggestedOnly: Boolean? = null, + autoJoinedOnly: Boolean? = null): Pair<RoomSummary, List<SpaceChildInfo>> + + /** + * Get a live list of space summaries. This list is refreshed as soon as the data changes. + * @return the [LiveData] of List[SpaceSummary] + */ + fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>> + + fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<RoomSummary> + + suspend fun joinSpace(spaceIdOrAlias: String, + reason: String? = null, + viaServers: List<String> = emptyList()): JoinSpaceResult + + suspend fun rejectInvite(spaceId: String, reason: String?) + +// fun getSpaceParentsOfRoom(roomId: String) : List<SpaceSummary> + + /** + * Let this room declare that it has a parent. + * @param canonical true if it should be the main parent of this room + * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced: + * if multiple are present the client should select the one with the lowest room ID, as determined via a lexicographic utf-8 ordering. + */ + suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List<String>) + + fun getRootSpaceSummaries(): List<RoomSummary> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..0c33cfa1e60949aeea120f94287bf021c14278d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt @@ -0,0 +1,70 @@ +/* + * 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.api.session.space.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * "content": { + * "via": ["example.com"], + * "order": "abcd", + * "default": true + * } + */ +@JsonClass(generateAdapter = true) +data class SpaceChildContent( + /** + * Key which gives a list of candidate servers that can be used to join the room + * Children where via is not present are ignored. + */ + @Json(name = "via") val via: List<String>? = null, + /** + * The order key is a string which is used to provide a default ordering of siblings in the room list. + * (Rooms are sorted based on a lexicographic ordering of order values; rooms with no order come last. + * orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7F (~), + * or consist of more than 50 characters, are forbidden and should be ignored if received.) + */ + @Json(name = "order") val order: String? = null, + /** + * The auto_join flag on a child listing allows a space admin to list the sub-spaces and rooms in that space which should + * be automatically joined by members of that space. + * (This is not a force-join, which are descoped for a future MSC; the user can subsequently part these room if they desire.) + */ + @Json(name = "auto_join") val autoJoin: Boolean? = false, + + /** + * If `suggested` is set to `true`, that indicates that the child should be advertised to + * members of the space by the client. This could be done by showing them eagerly + * in the room list. This is should be ignored if `auto_join` is set to `true`. + */ + @Json(name = "suggested") val suggested: Boolean? = false +) { + /** + * Orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7F (~), + * or consist of more than 50 characters, are forbidden and should be ignored if received.) + */ + fun validOrder(): String? { + return order + ?.takeIf { it.length <= 50 } + ?.takeIf { ORDER_VALID_CHAR_REGEX.matches(it) } + } + + companion object { + private val ORDER_VALID_CHAR_REGEX = "[ -~]+".toRegex() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..871a4949147b590d4a8d598123b7f19f8992975b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt @@ -0,0 +1,48 @@ +/* + * 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.api.session.space.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Rooms can claim parents via the m.space.parent state event. + * { + * "type": "m.space.parent", + * "state_key": "!space:example.com", + * "content": { + * "via": ["example.com"], + * "canonical": true, + * } + * } + */ +@JsonClass(generateAdapter = true) +data class SpaceParentContent( + /** + * Key which gives a list of candidate servers that can be used to join the parent. + * Parents where via is not present are ignored. + */ + @Json(name = "via") val via: List<String>? = null, + /** + * Canonical determines whether this is the main parent for the space. + * When a user joins a room with a canonical parent, clients may switch to view the room + * in the context of that space, peeking into it in order to find other rooms and group them together. + * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced: + * if multiple are present the client should select the one with the lowest room ID, as determined via a lexicographic utf-8 ordering. + */ + @Json(name = "canonical") val canonical: Boolean? = false +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt index 2c4c03b7d45d39b0623179d32708da77eb76b823..7a4231c277ebffa7cdb7c0006076a9427849fb85 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt @@ -20,6 +20,7 @@ import android.annotation.SuppressLint import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.util.safeCapitalize /** * Ref: https://github.com/matrix-org/matrix-doc/issues/1236 @@ -39,6 +40,6 @@ data class WidgetContent( @SuppressLint("DefaultLocale") fun getHumanName(): String { - return (name ?: type ?: "").capitalize() + return (name ?: type ?: "").safeCapitalize() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt index db229a6453e53bc35272c911ab0285c7a872ecdd..deb279eb95f063acf97575441ea68a2b122b10ae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -20,6 +20,7 @@ import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.user.model.User @@ -116,22 +117,22 @@ sealed class MatrixItem( var first = dn[startIndex] // LEFT-TO-RIGHT MARK - if (dn.length >= 2 && 0x200e == first.toInt()) { + if (dn.length >= 2 && 0x200e == first.code) { startIndex++ first = dn[startIndex] } // check if it’s the start of a surrogate pair - if (first.toInt() in 0xD800..0xDBFF && dn.length > startIndex + 1) { + if (first.code in 0xD800..0xDBFF && dn.length > startIndex + 1) { val second = dn[startIndex + 1] - if (second.toInt() in 0xDC00..0xDFFF) { + if (second.code in 0xDC00..0xDFFF) { length++ } } dn.substring(startIndex, startIndex + length) } - .toUpperCase(Locale.ROOT) + .uppercase(Locale.ROOT) } companion object { @@ -157,3 +158,5 @@ fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAl fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) + +fun SpaceChildInfo.toMatrixItem() = MatrixItem.RoomItem(childRoomId, name ?: canonicalAlias ?: "", avatarUrl) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt index c74999b4abbca2310c0ab41976a4d7ede124f22e..182b37f2ad018e609a1d480fabb3659c427c59de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.extensions.orFalse object MimeTypes { const val Any: String = "*/*" const val OctetStream = "application/octet-stream" + const val Apk = "application/vnd.android.package-archive" const val Images = "image/*" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt index f93f285c6eaca6e4a730bcb69d8a52aba30fb444..5a9fa9edf65263338e8ba47845099cba1914d3bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.auth import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.auth.data.Availability import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams @@ -73,6 +74,15 @@ internal interface AuthAPI { @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/available") suspend fun registerAvailable(@Query("username") username: String): Availability + /** + * Get the combined profile information for this user. + * This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers. + * This API may return keys which are not limited to displayname or avatar_url. + * @param userId the user id to fetch profile info + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}") + suspend fun getProfile(@Path("userId") userId: String): JsonDict + /** * Add 3Pid during registration * Ref: https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index e26286ad2f5b64bca88b469a69ddd4e6cd9a4196..46256f4b81fc05cb4f5735c2341cf0be24702a93 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -144,16 +144,14 @@ internal class DefaultAuthenticationService @Inject constructor( } return result.fold( { - if (it is LoginFlowResult.Success) { - // The homeserver exists and up to date, keep the config - // Homeserver url may have been changed, if it was a Riot url - val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy( - homeServerUri = Uri.parse(it.homeServerUrl) - ) - - pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig) - .also { data -> pendingSessionStore.savePendingSessionData(data) } - } + // The homeserver exists and up to date, keep the config + // Homeserver url may have been changed, if it was a Riot url + val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(it.homeServerUrl) + ) + + pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig) + .also { data -> pendingSessionStore.savePendingSessionData(data) } it }, { @@ -307,12 +305,12 @@ internal class DefaultAuthenticationService @Inject constructor( val loginFlowResponse = executeRequest(null) { authAPI.getLoginFlows() } - return LoginFlowResult.Success( - loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, - loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, - versions.isLoginAndRegistrationSupportedBySdk(), - homeServerUrl, - !versions.isSupportedBySdk() + return LoginFlowResult( + supportedLoginTypes = loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, + ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, + isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(), + homeServerUrl = homeServerUrl, + isOutdatedHomeserver = !versions.isSupportedBySdk() ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt index 8b81f42e03873f47c2108c90039141fe5c90f1fb..854caf8a622a90adf17c907dc4e36d658423da06 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.auth.login import android.util.Patterns +import org.matrix.android.sdk.api.auth.login.LoginProfileInfo import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.api.session.Session @@ -30,6 +31,7 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationParams import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver internal class DefaultLoginWizard( private val authAPI: AuthAPI, @@ -39,6 +41,15 @@ internal class DefaultLoginWizard( private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") + private val getProfileTask: GetProfileTask = DefaultGetProfileTask( + authAPI, + DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig) + ) + + override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo { + return getProfileTask.execute(GetProfileTask.Params(matrixId)) + } + override suspend fun login(login: String, password: String, deviceName: String): Session { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/GetProfileTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/GetProfileTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..bb9faf49c4a73b83aea6daf25033ceb23d41b63d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/GetProfileTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.login + +import org.matrix.android.sdk.api.auth.login.LoginProfileInfo +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.profile.ProfileService +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task + +internal interface GetProfileTask : Task<GetProfileTask.Params, LoginProfileInfo> { + data class Params( + val userId: String + ) +} + +internal class DefaultGetProfileTask( + private val authAPI: AuthAPI, + private val contentUrlResolver: ContentUrlResolver +) : GetProfileTask { + + override suspend fun execute(params: GetProfileTask.Params): LoginProfileInfo { + val info = executeRequest(null) { + authAPI.getProfile(params.userId) + } + + return LoginProfileInfo( + matrixId = params.userId, + displayName = info[ProfileService.DISPLAY_NAME_KEY] as? String, + fullAvatarUrl = contentUrlResolver.resolveFullSize(info[ProfileService.AVATAR_URL_KEY] as? String) + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt index 4a3d53a8fc9328b80da39bdc83ee6a2781fcd5b9..4a156e74cd994743205e42240a5d6f887c065b53 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -66,8 +66,8 @@ internal class DefaultRegistrationWizard( return performRegistrationRequest(params) } - override suspend fun createAccount(userName: String, - password: String, + override suspend fun createAccount(userName: String?, + password: String?, initialDeviceDisplayName: String?): RegistrationResult { val params = RegistrationParams( username = userName, 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 2163b2a5e0276046d414bb1e4da8ff4d5d112284..7f5cfe8df103cfa036eacae1bd760855dd2006eb 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 @@ -545,14 +545,14 @@ internal class DefaultCryptoService @Inject constructor( val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { - Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") + Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") return false } val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm) if (!encryptingClass) { - Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") + Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") return false } @@ -649,17 +649,17 @@ internal class DefaultCryptoService @Inject constructor( val safeAlgorithm = alg if (safeAlgorithm != null) { val t0 = System.currentTimeMillis() - Timber.v("## CRYPTO | encryptEventContent() starts") + Timber.v("## CRYPTO | encryptEventContent() starts") runCatching { val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) - Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") MXEncryptEventContentResult(content, EventType.ENCRYPTED) }.foldToCallback(callback) } else { val algorithm = getEncryptionAlgorithm(roomId) val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON) - Timber.e("## CRYPTO | encryptEventContent() : $reason") + Timber.e("## CRYPTO | encryptEventContent() : $reason") callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))) } } @@ -769,7 +769,7 @@ internal class DefaultCryptoService @Inject constructor( } val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) if (alg == null) { - Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") + Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") return } alg.onRoomKeyEvent(event, keysBackupService) @@ -777,7 +777,7 @@ internal class DefaultCryptoService @Inject constructor( private fun onKeyWithHeldReceived(event: Event) { val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also { - Timber.i("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") + Timber.i("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") } Timber.i("## CRYPTO | onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>") val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm) @@ -790,16 +790,16 @@ internal class DefaultCryptoService @Inject constructor( } private fun onSecretSendReceived(event: Event) { - Timber.i("## CRYPTO | GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}") + Timber.i("## CRYPTO | GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}") if (!event.isEncrypted()) { // secret send messages must be encrypted - Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event") + Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event") return } // Was that sent by us? if (event.senderId != userId) { - Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") + Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") return } @@ -809,13 +809,13 @@ internal class DefaultCryptoService @Inject constructor( .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId } if (existingRequest == null) { - Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") return } if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) { // TODO Ask to application layer? - Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK") + Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK") } } @@ -972,13 +972,13 @@ internal class DefaultCryptoService @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.main) { runCatching { withContext(coroutineDispatchers.crypto) { - Timber.v("## CRYPTO | importRoomKeys starts") + Timber.v("## CRYPTO | importRoomKeys starts") val t0 = System.currentTimeMillis() val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password) val t1 = System.currentTimeMillis() - Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") + Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") val importedSessions = MoshiProvider.providesMoshi() .adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java)) @@ -986,7 +986,7 @@ internal class DefaultCryptoService @Inject constructor( val t2 = System.currentTimeMillis() - Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms") + Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms") if (importedSessions == null) { throw Exception("Error") @@ -1125,7 +1125,7 @@ internal class DefaultCryptoService @Inject constructor( */ override fun reRequestRoomKeyForEvent(event: Event) { val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also { - Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content") + Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content") } val requestBody = RoomKeyRequestBody( @@ -1140,18 +1140,18 @@ internal class DefaultCryptoService @Inject constructor( override fun requestRoomKeyForEvent(event: Event) { val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also { - Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") + Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") } cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { // if (!isStarted()) { -// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init") +// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init") // internalStart(false) // } roomDecryptorProvider .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm) ?.requestKeysForEvent(event, false) ?: run { - Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") + Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") } } } @@ -1180,11 +1180,11 @@ internal class DefaultCryptoService @Inject constructor( // 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") +// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") // return // } // -// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") +// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") // lastNewSessionForcedDates.setObject(senderId, deviceKey, now) // // cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { @@ -1201,7 +1201,7 @@ internal class DefaultCryptoService @Inject constructor( // val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) // val sendToDeviceMap = MXUsersDevicesMap<Any>() // sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) -// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") +// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") // val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) // sendToDeviceTask.execute(sendToDeviceParams) // } @@ -1290,12 +1290,12 @@ internal class DefaultCryptoService @Inject constructor( override fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.d("## CRYPTO | prepareToEncrypt() : Check room members up to date") + Timber.d("## CRYPTO | prepareToEncrypt() : Check room members up to date") // Ensure to load all room members try { loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) } catch (failure: Throwable) { - Timber.e("## CRYPTO | prepareToEncrypt() : Failed to load room members") + Timber.e("## CRYPTO | prepareToEncrypt() : Failed to load room members") callback.onFailure(failure) return@launch } @@ -1308,7 +1308,7 @@ internal class DefaultCryptoService @Inject constructor( if (alg == null) { val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) - Timber.e("## CRYPTO | prepareToEncrypt() : $reason") + Timber.e("## CRYPTO | prepareToEncrypt() : $reason") callback.onFailure(IllegalArgumentException("Missing algorithm")) return@launch } @@ -1318,7 +1318,7 @@ internal class DefaultCryptoService @Inject constructor( }.fold( { callback.onSuccess(Unit) }, { - Timber.e("## CRYPTO | prepareToEncrypt() failed.") + Timber.e("## CRYPTO | prepareToEncrypt() failed.") callback.onFailure(it) } ) 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 e5f1c011f8c2c349ca9d4bac26eb67c8c7beb9b8..63f15aaf6e6ca0ce3555b566746f282191194696 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 @@ -111,7 +111,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM res = !notReadyToRetryHS.contains(userId.substringAfter(':')) } } catch (e: Exception) { - Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed") + Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed") } } @@ -150,7 +150,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM for (userId in userIds) { if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) { - Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId") + Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId") deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD isUpdated = true } @@ -178,7 +178,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM for (userId in changed) { if (deviceTrackingStatuses.containsKey(userId)) { - Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId") + Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId") deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD isUpdated = true } @@ -186,7 +186,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM for (userId in left) { if (deviceTrackingStatuses.containsKey(userId)) { - Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId") + Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId") deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED isUpdated = true } @@ -276,7 +276,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * @param forceDownload Always download the keys even if cached. */ suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> { - Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds") + Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds") // Map from userId -> deviceId -> MXDeviceInfo val stored = MXUsersDevicesMap<CryptoDeviceInfo>() @@ -305,13 +305,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } } return if (downloadUsers.isEmpty()) { - Timber.v("## CRYPTO | downloadKeys() : no new user device") + Timber.v("## CRYPTO | downloadKeys() : no new user device") stored } else { - Timber.v("## CRYPTO | downloadKeys() : starts") + Timber.v("## CRYPTO | downloadKeys() : starts") val t0 = System.currentTimeMillis() val result = doKeyDownloadForUsers(downloadUsers) - Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms") result.also { it.addEntriesFromMap(stored) } @@ -324,7 +324,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * @param downloadUsers the user ids list */ private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> { - Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}") + Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}") // get the user ids which did not already trigger a keys download val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) } if (filteredUsers.isEmpty()) { @@ -335,16 +335,16 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM val response = try { downloadKeysForUsersTask.execute(params) } catch (throwable: Throwable) { - Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") + Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") onKeysDownloadFailed(filteredUsers) throw throwable } - Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") + Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") for (userId in filteredUsers) { // al devices = val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } - Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models") + Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models") if (!models.isNullOrEmpty()) { val workingCopy = models.toMutableMap() for ((deviceId, deviceInfo) in models) { @@ -377,13 +377,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") } val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") } val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") } cryptoStore.storeUserCrossSigningKeys( userId, @@ -411,28 +411,28 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM */ private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean { if (null == deviceKeys) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId") + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId") return false } if (null == deviceKeys.keys) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId") + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId") return false } if (null == deviceKeys.signatures) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId") + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId") return false } // Check that the user_id and device_id in the received deviceKeys are correct if (deviceKeys.userId != userId) { - Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId") + Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId") return false } if (deviceKeys.deviceId != deviceId) { - Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId") + Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId") return false } @@ -440,21 +440,21 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM val signKey = deviceKeys.keys[signKeyId] if (null == signKey) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key") + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key") return false } val signatureMap = deviceKeys.signatures[userId] if (null == signatureMap) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId") + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId") return false } val signature = signatureMap[signKeyId] if (null == signature) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed") + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed") return false } @@ -469,7 +469,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } if (!isVerified) { - Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + deviceKeys.deviceId + " with error " + errorMessage) return false } @@ -480,12 +480,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM // best off sticking with the original keys. // // Should we warn the user about it somehow? - Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + deviceKeys.deviceId + " has changed : " + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) - Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") - Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") + Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") + Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") return false } @@ -499,7 +499,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * This method must be called on getEncryptingThreadHandler() thread. */ suspend fun refreshOutdatedDeviceLists() { - Timber.v("## CRYPTO | refreshOutdatedDeviceLists()") + Timber.v("## CRYPTO | refreshOutdatedDeviceLists()") val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId -> @@ -518,10 +518,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM doKeyDownloadForUsers(users) }.fold( { - Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done") + Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done") }, { - Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") + Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") } ) } 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 index 32324896fa3acd7868cb3cfadd83e4003f9e95df..8d86380e39e24a31b3a7206a131c726acd5e562e 100644 --- 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 @@ -92,20 +92,20 @@ internal class EventDecryptor @Inject constructor( private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { val eventContent = event.content if (eventContent == null) { - Timber.e("## CRYPTO | decryptEvent : empty event content") + 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") + 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.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") + Timber.v("## 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) { @@ -119,7 +119,7 @@ internal class EventDecryptor @Inject constructor( markOlmSessionForUnwedging(event.senderId ?: "", it) } ?: run { - Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging") + Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging") } } } @@ -137,18 +137,18 @@ internal class EventDecryptor @Inject constructor( val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 val now = System.currentTimeMillis() if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { - Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") + Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") return } - Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") + Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") lastNewSessionForcedDates.setObject(senderId, deviceKey, now) // offload this from crypto thread (?) cryptoCoroutineScope.launch(coroutineDispatchers.computation) { val ensured = ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) - Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}") + Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}") // 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) @@ -161,13 +161,13 @@ internal class EventDecryptor @Inject constructor( val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap<Any>() sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) - Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}") + Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}") withContext(coroutineDispatchers.io) { val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) try { sendToDeviceTask.execute(sendToDeviceParams) } catch (failure: Throwable) { - Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}") + Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}") } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 787d16defc718ebc2b45c24f99181f8ec870440b..a29ac457fb956178d2be73f9b13c53b112f0fc5e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -74,7 +74,7 @@ internal class MXMegolmDecryption(private val userId: String, @Throws(MXCryptoError::class) private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { - Timber.v("## CRYPTO | decryptEvent ${event.eventId} , requestKeysOnFail:$requestKeysOnFail") + Timber.v("## CRYPTO | decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail") if (event.roomId.isNullOrBlank()) { throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) } @@ -360,7 +360,7 @@ internal class MXMegolmDecryption(private val userId: String, }, { // TODO - Timber.e(it, "## CRYPTO | shareKeysWithDevice: failed to get session for request $body") + Timber.e(it, "## CRYPTO | shareKeysWithDevice: failed to get session for request $body") } ) 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 697711d05161d8440b14edaa9b9dac78c119654e..a7564444759960a157922029b4b68fbe3b89c911 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 @@ -80,9 +80,9 @@ internal class MXMegolmEncryption( eventType: String, userIds: List<String>): Content { val ts = System.currentTimeMillis() - Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom") + Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom") val devices = getDevicesInRoom(userIds) - Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}") + Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}") val outboundSession = ensureOutboundSession(devices.allowedDevices) return encryptContent(outboundSession, eventType, eventContent) @@ -91,7 +91,7 @@ internal class MXMegolmEncryption( // annoyingly we have to serialize again the saved outbound session to store message index :/ // if not we would see duplicate message index errors olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId) - Timber.v("## CRYPTO | encryptEventContent: Finished in ${System.currentTimeMillis() - ts} millis") + Timber.v("## CRYPTO | encryptEventContent: Finished in ${System.currentTimeMillis() - ts} millis") } } @@ -118,13 +118,13 @@ internal class MXMegolmEncryption( override suspend fun preshareKey(userIds: List<String>) { val ts = System.currentTimeMillis() - Timber.v("## CRYPTO | preshareKey : getDevicesInRoom") + Timber.v("## CRYPTO | preshareKey : getDevicesInRoom") val devices = getDevicesInRoom(userIds) val outboundSession = ensureOutboundSession(devices.allowedDevices) notifyWithheldForSession(devices.withHeldDevices, outboundSession) - Timber.v("## CRYPTO | preshareKey ${System.currentTimeMillis() - ts} millis") + Timber.v("## CRYPTO | preshareKey ${System.currentTimeMillis() - ts} millis") } /** @@ -133,7 +133,7 @@ internal class MXMegolmEncryption( * @return the session description */ private fun prepareNewSessionInRoom(): MXOutboundSessionInfo { - Timber.v("## CRYPTO | prepareNewSessionInRoom() ") + Timber.v("## CRYPTO | prepareNewSessionInRoom() ") val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId) val keysClaimedMap = HashMap<String, String>() @@ -153,7 +153,7 @@ internal class MXMegolmEncryption( * @param devicesInRoom the devices list */ private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo { - Timber.v("## CRYPTO | ensureOutboundSession start") + Timber.v("## CRYPTO | ensureOutboundSession start") var session = outboundSession if (session == null // Need to make a brand new session? @@ -190,7 +190,7 @@ internal class MXMegolmEncryption( devicesByUsers: Map<String, List<CryptoDeviceInfo>>) { // nothing to send, the task is done if (devicesByUsers.isEmpty()) { - Timber.v("## CRYPTO | shareKey() : nothing more to do") + Timber.v("## CRYPTO | shareKey() : nothing more to do") return } // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) @@ -203,7 +203,7 @@ internal class MXMegolmEncryption( break } } - Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}") + Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}") shareUserDevicesKey(session, subMap) val remainingDevices = devicesByUsers - subMap.keys shareKey(session, remainingDevices) @@ -232,11 +232,11 @@ internal class MXMegolmEncryption( payload["content"] = submap var t0 = System.currentTimeMillis() - Timber.v("## CRYPTO | shareUserDevicesKey() : starts") + Timber.v("## CRYPTO | shareUserDevicesKey() : starts") val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) Timber.v( - """## CRYPTO | shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms""" + """## CRYPTO | shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms""" .trimMargin() ) val contentMap = MXUsersDevicesMap<Any>() @@ -257,7 +257,7 @@ internal class MXMegolmEncryption( noOlmToNotify.add(UserDevice(userId, deviceID)) continue } - Timber.i("## CRYPTO | shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID") + Timber.i("## CRYPTO | shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID") contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) haveTargets = true } @@ -289,17 +289,17 @@ internal class MXMegolmEncryption( if (haveTargets) { t0 = System.currentTimeMillis() - Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target") + Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) try { sendToDeviceTask.execute(sendToDeviceParams) - Timber.i("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.i("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") } catch (failure: Throwable) { // What to do here... - Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") + Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") } } else { - Timber.i("## CRYPTO | shareUserDevicesKey() : no need to sharekey") + Timber.i("## CRYPTO | shareUserDevicesKey() : no need to sharekey") } if (noOlmToNotify.isNotEmpty()) { @@ -317,7 +317,7 @@ internal class MXMegolmEncryption( sessionId: String, senderKey: String?, code: WithHeldCode) { - Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId and code $code") + Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId and code $code") val withHeldContent = RoomKeyWithHeldContent( roomId = roomId, senderKey = senderKey, @@ -336,7 +336,7 @@ internal class MXMegolmEncryption( try { sendToDeviceTask.execute(params) } catch (failure: Throwable) { - Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") + Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") } } @@ -473,7 +473,7 @@ internal class MXMegolmEncryption( val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap<Any>() sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.i("## CRYPTO | reshareKey() : sending session $sessionId to $userId:$deviceId") + Timber.i("## CRYPTO | reshareKey() : sending session $sessionId to $userId:$deviceId") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) return try { sendToDeviceTask.execute(sendToDeviceParams) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt index 7eebbd9b2c912f47872a6c58f6f3ed4ca849ea61..4004294d973da3fdfa47dda69d1e9055e29c7522 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt @@ -44,7 +44,7 @@ data class CryptoDeviceInfo( */ fun fingerprint(): String? { return keys - ?.takeIf { !deviceId.isBlank() } + ?.takeIf { deviceId.isNotBlank() } ?.get("ed25519:$deviceId") } @@ -53,7 +53,7 @@ data class CryptoDeviceInfo( */ fun identityKey(): String? { return keys - ?.takeIf { !deviceId.isBlank() } + ?.takeIf { deviceId.isNotBlank() } ?.get("curve25519:$deviceId") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt index 3c651c27a0810d34fbeac4f090d47451ed0841fd..00b8bde5d999aea1a843e537bfedab6ca7ce048f 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt @@ -103,7 +103,7 @@ data class MXDeviceInfo( */ fun fingerprint(): String? { return keys - ?.takeIf { !deviceId.isBlank() } + ?.takeIf { deviceId.isNotBlank() } ?.get("ed25519:$deviceId") } @@ -112,7 +112,7 @@ data class MXDeviceInfo( */ fun identityKey(): String? { return keys - ?.takeIf { !deviceId.isBlank() } + ?.takeIf { deviceId.isNotBlank() } ?.get("curve25519:$deviceId") } 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 d8b9d3cd864a91050a139fa365d84fd32a65380a..7fa48c3da1b56318f7d031cd7d1767eff24d4e70 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 @@ -23,6 +23,7 @@ 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.task.Task +import org.matrix.android.sdk.internal.util.toMatrixErrorStr import javax.inject.Inject internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> { @@ -55,7 +56,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( localEchoRepository.updateSendState(localId, event.roomId, SendState.SENT) return response.eventId } catch (e: Throwable) { - localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED) + localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED, e.toMatrixErrorStr()) throw e } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt index 6d52e682bc9c013225397d1ce2066ea18c656419..6839ccd3262c5679c27d3e977196a0405f0e1752 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt @@ -57,7 +57,7 @@ object HkdfSha256 { /* The output OKM is calculated as follows: - Notation | -> When the message is composed of several elements we use concatenation (denoted |) in the second argument; + Notation | -> When the message is composed of several elements we use concatenation (denoted |) in the second argument; N = ceil(L/HashLen) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt index c7885ce449edbbfd20e7708dba3664782b132e48..4bf01a28095a6cbf390dc43b5d82623726f1e0d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -345,7 +345,7 @@ internal abstract class SASDefaultVerificationTransaction( } protected fun hashUsingAgreedHashMethod(toHash: String): String? { - if ("sha256" == accepted?.hash?.toLowerCase(Locale.ROOT)) { + if ("sha256" == accepted?.hash?.lowercase(Locale.ROOT)) { val olmUtil = OlmUtility() val hashBytes = olmUtil.sha256(toHash) olmUtil.releaseUtility() @@ -355,7 +355,7 @@ internal abstract class SASDefaultVerificationTransaction( } private fun macUsingAgreedMethod(message: String, info: String): String? { - return when (accepted?.messageAuthenticationCode?.toLowerCase(Locale.ROOT)) { + return when (accepted?.messageAuthenticationCode?.lowercase(Locale.ROOT)) { SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info) SAS_MAC_SHA256 -> getSAS().calculateMac(message, info) else -> null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt index 6bc3483e651ad5958a35ce34300e3527e32971c1..76e88442b60759cc492971feffdcf801a883708e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt @@ -48,7 +48,7 @@ fun QrCodeData.toEncodedString(): String { // TransactionId transactionId.forEach { - result += it.toByte() + result += it.code.toByte() } // Keys diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt index f11ecc5d75f8b6209b0739bca723e0e034318881..ee58880eb833a1bea6ef0ef557ab746e5fb0ac4d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt @@ -20,6 +20,7 @@ import io.realm.Realm import io.realm.RealmConfiguration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.internal.database.helper.nextDisplayIndex import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntityFields @@ -29,7 +30,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.model.deleteOnCascade import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.task.TaskExecutor import timber.log.Timber @@ -47,7 +48,7 @@ private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300 internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor) : SessionLifecycleObserver { - override fun onSessionStarted() { + override fun onSessionStarted(session: Session) { taskExecutor.executorScope.launch(Dispatchers.Default) { awaitTransaction(realmConfiguration) { realm -> val allRooms = realm.where(RoomEntity::class.java).findAll() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt index 0e3a7a2c49b602f1017e3db7beefd9e65e401ec9..d5ff7a0f84975bd2618a27de25ebfe36fe7d5473 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt @@ -71,7 +71,7 @@ internal class RealmKeysUtils @Inject constructor(context: Context, val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING) val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias) sharedPreferences.edit { - putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING)) + putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore, Base64.NO_PADDING)) } return key } @@ -84,7 +84,7 @@ internal class RealmKeysUtils @Inject constructor(context: Context, val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null) val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING) val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias) - return Base64.decode(b64!!, Base64.NO_PADDING) + return Base64.decode(b64, Base64.NO_PADDING) } fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt index 2a0cd963b2506c5a1f8547993661766a1899bad5..c602ed7075919addb62c6e4fbcb08a5caf53bf69 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.database import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.util.createBackgroundHandler import io.realm.Realm import io.realm.RealmChangeListener @@ -28,6 +28,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.cancelChildren +import org.matrix.android.sdk.api.session.Session import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference @@ -46,7 +47,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r private val backgroundRealm = AtomicReference<Realm>() private lateinit var results: AtomicReference<RealmResults<T>> - override fun onSessionStarted() { + override fun onSessionStarted(session: Session) { if (isStarted.compareAndSet(false, true)) { BACKGROUND_HANDLER.post { val realm = Realm.getInstance(realmConfiguration) @@ -58,7 +59,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r } } - override fun onSessionStopped() { + override fun onSessionStopped(session: Session) { if (isStarted.compareAndSet(true, false)) { BACKGROUND_HANDLER.post { results.getAndSet(null).removeAllChangeListeners() @@ -70,7 +71,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r } } - override fun onClearCache() { + override fun onClearCache(session: Session) { observerScope.coroutineContext.cancelChildren() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt index f8d5d323a5a4f9015709cbd84773b6cea57ae682..52fbabb49fe85e220fbfa8347f6ab39b5c203821 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt @@ -20,8 +20,9 @@ import android.os.Looper import androidx.annotation.MainThread import com.zhuinden.monarchy.Monarchy import io.realm.Realm +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope import javax.inject.Inject import kotlin.concurrent.getOrSet @@ -44,14 +45,14 @@ internal class RealmSessionProvider @Inject constructor(@SessionDatabase private } @MainThread - override fun onSessionStarted() { + override fun onSessionStarted(session: Session) { realmThreadLocal.getOrSet { Realm.getInstance(monarchy.realmConfiguration) } } @MainThread - override fun onSessionStopped() { + override fun onSessionStopped(session: Session) { realmThreadLocal.get()?.close() realmThreadLocal.remove() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 1daae906f2e902de3d302c776a46a29e4734861d..d810c8b1a8bb85d47a0e9e38b0952bb38fab1026 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -19,7 +19,11 @@ package org.matrix.android.sdk.internal.database import io.realm.DynamicRealm import io.realm.FieldAttribute import io.realm.RealmMigration +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.EditionOfEventFields import org.matrix.android.sdk.internal.database.model.EventEntityFields @@ -30,14 +34,17 @@ import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields +import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.di.MoshiProvider import timber.log.Timber import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 9L + const val SESSION_STORE_SCHEMA_VERSION = 13L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -52,6 +59,10 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 6) migrateTo7(realm) if (oldVersion <= 7) migrateTo8(realm) if (oldVersion <= 8) migrateTo9(realm) + if (oldVersion <= 9) migrateTo10(realm) + if (oldVersion <= 10) migrateTo11(realm) + if (oldVersion <= 11) migrateTo12(realm) + if (oldVersion <= 12) migrateTo13(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -136,10 +147,6 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { Timber.d("Step 7 -> 8") val editionOfEventSchema = realm.schema.create("EditionOfEvent") - .apply { - // setEmbedded does not return `this`... - isEmbedded = true - } .addField(EditionOfEventFields.CONTENT, String::class.java) .addField(EditionOfEventFields.EVENT_ID, String::class.java) .setRequired(EditionOfEventFields.EVENT_ID, true) @@ -154,9 +161,13 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { ?.removeField("lastEditTs") ?.removeField("sourceLocalEchoEvents") ?.addRealmListField(EditAggregatedSummaryEntityFields.EDITIONS.`$`, editionOfEventSchema) + + // This has to be done once a parent use the model as a child + // See https://github.com/realm/realm-java/issues/7402 + editionOfEventSchema.isEmbedded = true } - fun migrateTo9(realm: DynamicRealm) { + private fun migrateTo9(realm: DynamicRealm) { Timber.d("Step 8 -> 9") realm.schema.get("RoomSummaryEntity") @@ -174,7 +185,6 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { ?.addIndex(RoomSummaryEntityFields.IS_SERVER_NOTICE) ?.transform { obj -> - val isFavorite = obj.getList(RoomSummaryEntityFields.TAGS.`$`).any { it.getString(RoomTagEntityFields.TAG_NAME) == RoomTag.ROOM_TAG_FAVOURITE } @@ -194,4 +204,85 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { } } } + + private fun migrateTo10(realm: DynamicRealm) { + Timber.d("Step 9 -> 10") + realm.schema.create("SpaceChildSummaryEntity") + ?.addField(SpaceChildSummaryEntityFields.ORDER, String::class.java) + ?.addField(SpaceChildSummaryEntityFields.CHILD_ROOM_ID, String::class.java) + ?.addField(SpaceChildSummaryEntityFields.AUTO_JOIN, Boolean::class.java) + ?.setNullable(SpaceChildSummaryEntityFields.AUTO_JOIN, true) + ?.addRealmObjectField(SpaceChildSummaryEntityFields.CHILD_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) + ?.addRealmListField(SpaceChildSummaryEntityFields.VIA_SERVERS.`$`, String::class.java) + + realm.schema.create("SpaceParentSummaryEntity") + ?.addField(SpaceParentSummaryEntityFields.PARENT_ROOM_ID, String::class.java) + ?.addField(SpaceParentSummaryEntityFields.CANONICAL, Boolean::class.java) + ?.setNullable(SpaceParentSummaryEntityFields.CANONICAL, true) + ?.addRealmObjectField(SpaceParentSummaryEntityFields.PARENT_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) + ?.addRealmListField(SpaceParentSummaryEntityFields.VIA_SERVERS.`$`, String::class.java) + + val creationContentAdapter = MoshiProvider.providesMoshi().adapter(RoomCreateContent::class.java) + realm.schema.get("RoomSummaryEntity") + ?.addField(RoomSummaryEntityFields.ROOM_TYPE, String::class.java) + ?.addField(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, String::class.java) + ?.addField(RoomSummaryEntityFields.GROUP_IDS, String::class.java) + ?.transform { obj -> + + val creationEvent = realm.where("CurrentStateEventEntity") + .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID)) + .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_CREATE) + .findFirst() + + val roomType = creationEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`) + ?.getString(EventEntityFields.CONTENT)?.let { + creationContentAdapter.fromJson(it)?.type + } + + obj.setString(RoomSummaryEntityFields.ROOM_TYPE, roomType) + } + ?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!) + ?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!) + } + + private fun migrateTo11(realm: DynamicRealm) { + Timber.d("Step 10 -> 11") + realm.schema.get("EventEntity") + ?.addField(EventEntityFields.SEND_STATE_DETAILS, String::class.java) + } + + private fun migrateTo12(realm: DynamicRealm) { + Timber.d("Step 11 -> 12") + + val joinRulesContentAdapter = MoshiProvider.providesMoshi().adapter(RoomJoinRulesContent::class.java) + realm.schema.get("RoomSummaryEntity") + ?.addField(RoomSummaryEntityFields.JOIN_RULES_STR, String::class.java) + ?.transform { obj -> + val joinRulesEvent = realm.where("CurrentStateEventEntity") + .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID)) + .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_JOIN_RULES) + .findFirst() + + val roomJoinRules = joinRulesEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`) + ?.getString(EventEntityFields.CONTENT)?.let { + joinRulesContentAdapter.fromJson(it)?.joinRules + } + + obj.setString(RoomSummaryEntityFields.JOIN_RULES_STR, roomJoinRules?.name) + } + + realm.schema.get("SpaceChildSummaryEntity") + ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java) + ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true) + } + + private fun migrateTo13(realm: DynamicRealm) { + Timber.d("Step 12 -> 13") + + // Fix issue with the nightly build. Eventually play again the migration which has been included in migrateTo12() + realm.schema.get("SpaceChildSummaryEntity") + ?.takeIf { !it.hasField(SpaceChildSummaryEntityFields.SUGGESTED) } + ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java) + ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index a4a2fadd21d9045e7fefe5eab3a7f132cfcce6d7..613b38e3403b1023bc9a9bb9fa46b8b23311c6c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -80,6 +80,7 @@ internal object EventMapper { ).also { it.ageLocalTs = eventEntity.ageLocalTs it.sendState = eventEntity.sendState + it.sendStateDetails = eventEntity.sendStateDetails eventEntity.decryptionResultJson?.let { json -> try { it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 6dc70b60fc71937647211b6d58b8a9019096e395..3fea15bd3dd7facf9edb659156506b9a37b9b2b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -17,6 +17,8 @@ package org.matrix.android.sdk.internal.database.mapper import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker @@ -42,7 +44,9 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa name = roomSummaryEntity.name ?: "", topic = roomSummaryEntity.topic ?: "", avatarUrl = roomSummaryEntity.avatarUrl ?: "", + joinRules = roomSummaryEntity.joinRules, isDirect = roomSummaryEntity.isDirect, + directUserId = roomSummaryEntity.directUserId, latestPreviewableEvent = latestEvent, joinedMembersCount = roomSummaryEntity.joinedMembersCount, invitedMembersCount = roomSummaryEntity.invitedMembersCount, @@ -63,7 +67,35 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex, roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, inviterId = roomSummaryEntity.inviterId, - hasFailedSending = roomSummaryEntity.hasFailedSending + hasFailedSending = roomSummaryEntity.hasFailedSending, + roomType = roomSummaryEntity.roomType, + spaceParents = roomSummaryEntity.parents.map { relationInfoEntity -> + SpaceParentInfo( + parentId = relationInfoEntity.parentRoomId, + roomSummary = relationInfoEntity.parentSummaryEntity?.let { map(it) }, + canonical = relationInfoEntity.canonical ?: false, + viaServers = relationInfoEntity.viaServers.toList() + ) + }, + spaceChildren = roomSummaryEntity.children.map { + SpaceChildInfo( + childRoomId = it.childRoomId ?: "", + isKnown = it.childSummaryEntity != null, + roomType = it.childSummaryEntity?.roomType, + name = it.childSummaryEntity?.name, + topic = it.childSummaryEntity?.topic, + avatarUrl = it.childSummaryEntity?.avatarUrl, + activeMemberCount = it.childSummaryEntity?.joinedMembersCount, + order = it.order, + autoJoin = it.autoJoin ?: false, + viaServers = it.viaServers.toList(), + parentRoomId = roomSummaryEntity.roomId, + suggested = it.suggested, + canonicalAlias = it.childSummaryEntity?.canonicalAlias, + aliases = it.childSummaryEntity?.aliases?.toList() + ) + }, + flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList() ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index fe59f4fceb03edf412267caac73cf3ae4a680d9c..c9edbcd889b6cdc6ab1d40d705ff904fa44dc3fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -32,6 +32,8 @@ internal open class EventEntity(@Index var eventId: String = "", @Index var stateKey: String? = null, var originServerTs: Long? = null, @Index var sender: String? = null, + // Can contain a serialized MatrixError + var sendStateDetails: String? = null, var age: Long? = 0, var unsignedData: String? = null, var redacts: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt index 3ff2532604803b7d9c0080ec0174e038f55c9be0..58297776f06b1f47189aee4cf22ad3ffa718abc1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt @@ -43,6 +43,5 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "", set(value) { membersLoadStatusStr = value.name } - companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt index c87ac15a78337778b9be15fe13f98740349d7ea7..1001f9cd66ea11d0ae57d6cdd265478d6406c467 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -21,13 +21,18 @@ import io.realm.RealmObject import io.realm.annotations.Index import io.realm.annotations.PrimaryKey import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.tag.RoomTag internal open class RoomSummaryEntity( - @PrimaryKey var roomId: String = "" + @PrimaryKey var roomId: String = "", + var roomType: String? = null, + var parents: RealmList<SpaceParentSummaryEntity> = RealmList(), + var children: RealmList<SpaceChildSummaryEntity> = RealmList() ) : RealmObject() { var displayName: String? = "" @@ -204,6 +209,16 @@ internal open class RoomSummaryEntity( if (value != field) field = value } + var flattenParentIds: String? = null + set(value) { + if (value != field) field = value + } + + var groupIds: String? = null + set(value) { + if (value != field) field = value + } + @Index private var membershipStr: String = Membership.NONE.name @@ -229,6 +244,19 @@ internal open class RoomSummaryEntity( } } + private var joinRulesStr: String? = null + var joinRules: RoomJoinRules? + get() { + return joinRulesStr?.let { + tryOrNull { RoomJoinRules.valueOf(it) } + } + } + set(value) { + if (value?.name != joinRulesStr) { + joinRulesStr = value?.name + } + } + var roomEncryptionTrustLevel: RoomEncryptionTrustLevel? get() { return roomEncryptionTrustLevelStr?.let { @@ -244,6 +272,5 @@ internal open class RoomSummaryEntity( roomEncryptionTrustLevelStr = value?.name } } - companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 6e6096cf8a62111742d2306224ff91a125830b97..72ae512fa5a444529809da11b63028355ba4251e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -61,6 +61,8 @@ import io.realm.annotations.RealmModule CurrentStateEventEntity::class, UserAccountDataEntity::class, ScalarTokenEntity::class, - WellknownIntegrationManagerConfigEntity::class + WellknownIntegrationManagerConfigEntity::class, + SpaceChildSummaryEntity::class, + SpaceParentSummaryEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..ce1afbb507417d905faf80c93556e2043c53588c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Decorates room summary with space related information. + */ +internal open class SpaceChildSummaryEntity( +// var isSpace: Boolean = false, + + var order: String? = null, + + var autoJoin: Boolean? = null, + + var suggested: Boolean? = null, + + var childRoomId: String? = null, + // Link to the actual space summary if it is known locally + var childSummaryEntity: RoomSummaryEntity? = null, + + var viaServers: RealmList<String> = RealmList() +// var owner: RoomSummaryEntity? = null, + +// var level: Int = 0 + +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..30517717f4393927c7d8005aa589ed8e32c3cd9a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Decorates room summary with space related information. + */ +internal open class SpaceParentSummaryEntity( + /** + * Determines whether this is the main parent for the space + * When a user joins a room with a canonical parent, clients may switch to view the room in the context of that space, + * peeking into it in order to find other rooms and group them together. + * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced: + * if multiple are present the client should select the one with the lowest room ID, + * as determined via a lexicographic utf-8 ordering. + */ + var canonical: Boolean? = null, + + var parentRoomId: String? = null, + // Link to the actual space summary if it is known locally + var parentSummaryEntity: RoomSummaryEntity? = null, + + var viaServers: RealmList<String> = RealmList() + +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt index 9d6fa29bb278a83a3ec1f66469edbdf3ea4cc309..5bc519e96049aede5d142aff61e37cb462069f28 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.TestInterceptor import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.system.SystemModule import org.matrix.olm.OlmManager import java.io.File @@ -44,6 +45,7 @@ import java.io.File NetworkModule::class, AuthModule::class, RawModule::class, + SystemModule::class, NoOpTestModule::class ]) @MatrixScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt index 0246bae024eed8933e5f2224939ad9655d652a1e..e045cebd3ed0c1cb55043c8e33a660a8b7f1fde7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt @@ -88,8 +88,8 @@ internal suspend inline fun <DATA> executeRequest(globalErrorReceiver: GlobalErr throw when (exception) { is IOException -> Failure.NetworkConnection(exception) is Failure.ServerError, - is Failure.OtherServerError -> exception - is CancellationException -> Failure.Cancelled(exception) + is Failure.OtherServerError, + is CancellationException -> exception else -> Failure.Unknown(exception) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt index 7132b4ff7a8518fb034d543f646953629f2bc203..2116063626b16d2758b3427022b11c90a8373b9f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.internal.di.MoshiProvider import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.ResponseBody +import org.matrix.android.sdk.api.extensions.orFalse import retrofit2.HttpException import retrofit2.Response import timber.log.Timber @@ -91,7 +92,7 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int, globalErrorReceiv } else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ && matrixError.code == MatrixError.M_UNKNOWN_TOKEN) { // Also send this error to the globalErrorReceiver, for a global management - globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout)) + globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout.orFalse())) } return Failure.ServerError(matrixError, httpCode) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryRoomOrderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryRoomOrderProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..7a06c2129c0cb12a3bc7a326437663a86791122c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryRoomOrderProcessor.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.query + +import io.realm.RealmQuery +import io.realm.Sort +import org.matrix.android.sdk.api.session.room.RoomSortOrder +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields + +internal fun RealmQuery<RoomSummaryEntity>.process(sortOrder: RoomSortOrder): RealmQuery<RoomSummaryEntity> { + when (sortOrder) { + RoomSortOrder.NAME -> { + sort(RoomSummaryEntityFields.DISPLAY_NAME, Sort.ASCENDING) + } + RoomSortOrder.ACTIVITY -> { + sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING) + } + RoomSortOrder.NONE -> { + } + } + return this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt index 899024458a548af33fb73b355c3ec2cfa5664b83..fd3368223139837781fe7a2f97d64d6f48183d1e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.internal.query -import org.matrix.android.sdk.api.query.QueryStringValue import io.realm.Case import io.realm.RealmObject import io.realm.RealmQuery +import org.matrix.android.sdk.api.query.QueryStringValue import timber.log.Timber fun <T : RealmObject> RealmQuery<T>.process(field: String, queryStringValue: QueryStringValue): RealmQuery<T> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index d05ee48c1bf41c33e520600100756af615e8ef3a..a284d976d0525187ce18e5306e488f7a29f2cd6f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -219,7 +219,7 @@ internal class DefaultFileService @Inject constructor( fileName: String, mimeType: String?, elementToDecrypt: ElementToDecrypt?): Boolean { - return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE + return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) is FileService.FileState.InCache } internal data class CachedFiles( @@ -256,12 +256,17 @@ internal class DefaultFileService @Inject constructor( fileName: String, mimeType: String?, elementToDecrypt: ElementToDecrypt?): FileService.FileState { - mxcUrl ?: return FileService.FileState.UNKNOWN - if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).file.exists()) return FileService.FileState.IN_CACHE + mxcUrl ?: return FileService.FileState.Unknown + val files = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null) + if (files.file.exists()) { + return FileService.FileState.InCache( + decryptedFileInCache = files.getClearFile().exists() + ) + } val isDownloading = synchronized(ongoing) { ongoing[mxcUrl] != null } - return if (isDownloading) FileService.FileState.DOWNLOADING else FileService.FileState.UNKNOWN + return if (isDownloading) FileService.FileState.Downloading else FileService.FileState.Unknown } /** @@ -286,7 +291,7 @@ internal class DefaultFileService @Inject constructor( Timber.v("Get size of ${it.absolutePath}") true } - .sumBy { it.length().toInt() } + .sumOf { it.length().toInt() } } override fun clearCache() { 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 821a9cba8c58f8b24493de43c5a10232b6094e53..b100a336a79b2c3cc909312bd1941391d47706db 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 @@ -19,13 +19,15 @@ package org.matrix.android.sdk.internal.session import androidx.annotation.MainThread import dagger.Lazy import io.realm.RealmConfiguration +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.federation.FederationService import org.matrix.android.sdk.api.pushrules.PushRuleService -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.api.session.account.AccountService import org.matrix.android.sdk.api.session.accountdata.AccountDataService import org.matrix.android.sdk.api.session.cache.CacheService @@ -38,8 +40,10 @@ import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -49,6 +53,7 @@ import org.matrix.android.sdk.api.session.search.SearchService 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.signout.SignOutService +import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService @@ -120,6 +125,8 @@ internal class DefaultSession @Inject constructor( private val integrationManagerService: IntegrationManagerService, private val thirdPartyService: Lazy<ThirdPartyService>, private val callSignalingService: Lazy<CallSignalingService>, + private val spaceService: Lazy<SpaceService>, + private val openIdService: Lazy<OpenIdService>, @UnauthenticatedWithCertificate private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient> ) : Session, @@ -159,7 +166,12 @@ internal class DefaultSession @Inject constructor( isOpen = true cryptoService.get().ensureDevice() uiHandler.post { - lifecycleObservers.forEach { it.onSessionStarted() } + lifecycleObservers.forEach { + it.onSessionStarted(this) + } + sessionListeners.dispatch { + it.onSessionStarted(this) + } } globalErrorHandler.listener = this } @@ -200,7 +212,10 @@ internal class DefaultSession @Inject constructor( stopSync() // timelineEventDecryptor.destroy() uiHandler.post { - lifecycleObservers.forEach { it.onSessionStopped() } + lifecycleObservers.forEach { it.onSessionStopped(this) } + sessionListeners.dispatch { + it.onSessionStopped(this) + } } cryptoService.get().close() isOpen = false @@ -225,14 +240,23 @@ internal class DefaultSession @Inject constructor( stopSync() stopAnyBackgroundSync() uiHandler.post { - lifecycleObservers.forEach { it.onClearCache() } + lifecycleObservers.forEach { + it.onClearCache(this) + } + sessionListeners.dispatch { + it.onClearCache(this) + } + } + withContext(NonCancellable) { + cacheService.get().clearCache() } - cacheService.get().clearCache() workManagerProvider.cancelAllWorks() } override fun onGlobalError(globalError: GlobalError) { - sessionListeners.dispatchGlobalError(globalError) + sessionListeners.dispatch { + it.onGlobalError(this, globalError) + } } override fun contentUrlResolver() = contentUrlResolver @@ -265,6 +289,10 @@ internal class DefaultSession @Inject constructor( override fun thirdPartyService(): ThirdPartyService = thirdPartyService.get() + override fun spaceService(): SpaceService = spaceService.get() + + override fun openIdService(): OpenIdService = openIdService.get() + override fun getOkHttpClient(): OkHttpClient { return unauthenticatedWithCertificateOkHttpClient.get() } 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 7e1e3d0f7097b89daefecebc24c5fd3d89a0c0fe..9a936b73c2346b0ac2d63d49a62b906a66bb8350 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 @@ -52,6 +52,7 @@ import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker import org.matrix.android.sdk.internal.session.room.send.SendEventWorker import org.matrix.android.sdk.internal.session.search.SearchModule import org.matrix.android.sdk.internal.session.signout.SignOutModule +import org.matrix.android.sdk.internal.session.space.SpaceModule import org.matrix.android.sdk.internal.session.sync.SyncModule import org.matrix.android.sdk.internal.session.sync.SyncTask import org.matrix.android.sdk.internal.session.sync.SyncTokenStore @@ -63,6 +64,7 @@ import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModul import org.matrix.android.sdk.internal.session.widgets.WidgetModule import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.system.SystemModule @Component(dependencies = [MatrixComponent::class], modules = [ @@ -79,6 +81,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers CacheModule::class, MediaModule::class, CryptoModule::class, + SystemModule::class, PushersModule::class, OpenIdModule::class, WidgetModule::class, @@ -91,7 +94,8 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers FederationModule::class, CallModule::class, SearchModule::class, - ThirdPartyModule::class + ThirdPartyModule::class, + SpaceModule::class ] ) @SessionScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionCoroutineScopeHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionCoroutineScopeHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..82a8f79fd51217542add0dd24d6fdc9887f53183 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionCoroutineScopeHolder.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import javax.inject.Inject +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.SessionLifecycleObserver + +@SessionScope +internal class SessionCoroutineScopeHolder @Inject constructor(): SessionLifecycleObserver { + + val scope: CoroutineScope = CoroutineScope(SupervisorJob()) + + override fun onSessionStopped(session: Session) { + scope.cancelChildren() + } + + override fun onClearCache(session: Session) { + scope.cancelChildren() + } + + private fun CoroutineScope.cancelChildren() { + coroutineContext.cancelChildren(CancellationException("Closing session")) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt index 64f2d249f33cfc826d408b4b946dd8ac330b7a14..563ff4ada322cb827702b3ef7612e1e25a99caad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.session -import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.session.Session import javax.inject.Inject @@ -36,10 +35,10 @@ internal class SessionListeners @Inject constructor() { } } - fun dispatchGlobalError(globalError: GlobalError) { + fun dispatch(block: (Session.Listener) -> Unit) { synchronized(listeners) { listeners.forEach { - it.onGlobalError(globalError) + block(it) } } } 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 e61e4ecd893270b597253975efa76401423d1c26..de74b3481822893085a9c3ed49e11ac318ba7f43 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 @@ -33,10 +33,12 @@ import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.sessionId import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.api.session.accountdata.AccountDataService import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.openid.OpenIdService 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 @@ -81,6 +83,7 @@ import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapab import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.initsync.DefaultInitialSyncProgressService import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager +import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor @@ -343,6 +346,10 @@ internal abstract class SessionModule { @IntoSet abstract fun bindRealmSessionProvider(provider: RealmSessionProvider): SessionLifecycleObserver + @Binds + @IntoSet + abstract fun bindSessionCoroutineScopeHolder(holder: SessionCoroutineScopeHolder): SessionLifecycleObserver + @Binds @IntoSet abstract fun bindEventSenderProcessorAsSessionLifecycleObserver(processor: EventSenderProcessorCoroutine): SessionLifecycleObserver @@ -368,6 +375,9 @@ internal abstract class SessionModule { @Binds abstract fun bindPermalinkService(service: DefaultPermalinkService): PermalinkService + @Binds + abstract fun bindOpenIdTokenService(service: DefaultOpenIdService): OpenIdService + @Binds abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt index 754f12bd684831f8d57b390331d4e72e49a92e9d..17e0a930c1424756721fb91f5cac4ee4a1a3b667 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt @@ -78,6 +78,16 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU updateState(key, progressData) } + internal fun setCompressingImage(key: String) { + val progressData = ContentUploadStateTracker.State.CompressingImage + updateState(key, progressData) + } + + internal fun setCompressingVideo(key: String, percent: Float) { + val progressData = ContentUploadStateTracker.State.CompressingVideo(percent) + updateState(key, progressData) + } + internal fun setProgress(key: String, current: Long, total: Long) { val progressData = ContentUploadStateTracker.State.Uploading(current, total) updateState(key, progressData) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index 8fa595db3044188d23787eb055db1ab4a514f5d0..6a4dd2639206113062b28cb0fa10fdcc874b9618 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -31,24 +31,31 @@ import okhttp3.RequestBody.Companion.toRequestBody import okio.BufferedSink import okio.source import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.ProgressRequestBody import org.matrix.android.sdk.internal.network.awaitResponse import org.matrix.android.sdk.internal.network.toFailure +import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService +import org.matrix.android.sdk.internal.util.TemporaryFileCreator import java.io.File import java.io.FileNotFoundException import java.io.IOException -import java.util.UUID import javax.inject.Inject -internal class FileUploader @Inject constructor(@Authenticated - private val okHttpClient: OkHttpClient, - private val globalErrorReceiver: GlobalErrorReceiver, - private val context: Context, - contentUrlResolver: ContentUrlResolver, - moshi: Moshi) { +internal class FileUploader @Inject constructor( + @Authenticated private val okHttpClient: OkHttpClient, + private val globalErrorReceiver: GlobalErrorReceiver, + private val homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService, + private val context: Context, + private val temporaryFileCreator: TemporaryFileCreator, + contentUrlResolver: ContentUrlResolver, + moshi: Moshi +) { private val uploadUrl = contentUrlResolver.uploadUrl private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) @@ -57,6 +64,21 @@ internal class FileUploader @Inject constructor(@Authenticated filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { + // Check size limit + val maxUploadFileSize = homeServerCapabilitiesService.getHomeServerCapabilities().maxUploadFileSize + + if (maxUploadFileSize != HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN + && file.length() > maxUploadFileSize) { + // Known limitation and file too big for the server, save the pain to upload it + throw Failure.ServerError( + error = MatrixError( + code = MatrixError.M_TOO_LARGE, + message = "Cannot upload files larger than ${maxUploadFileSize / 1048576L}mb" + ), + httpCode = 413 + ) + } + val uploadBody = object : RequestBody() { override fun contentLength() = file.length() @@ -90,7 +112,7 @@ internal class FileUploader @Inject constructor(@Authenticated val inputStream = withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) } ?: throw FileNotFoundException() - val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + val workingFile = temporaryFileCreator.create() workingFile.outputStream().use { inputStream.copyTo(it) } @@ -99,11 +121,17 @@ internal class FileUploader @Inject constructor(@Authenticated } } - private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { + private suspend fun upload(uploadBody: RequestBody, + filename: String?, + progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException() val httpUrl = urlBuilder - .addQueryParameter("filename", filename) + .apply { + if (filename != null) { + addQueryParameter("filename", filename) + } + } .build() val requestBody = if (progressListener != null) ProgressRequestBody(uploadBody, progressListener) else uploadBody diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt index 1d6cd61060ff6dd7c91d2ab723b3066351f7b4fd..9b01d0a00e033fa6976c79dba71acdcdd0a17cf9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt @@ -16,19 +16,20 @@ package org.matrix.android.sdk.internal.session.content -import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix import androidx.exifinterface.media.ExifInterface import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.matrix.android.sdk.internal.util.TemporaryFileCreator import timber.log.Timber import java.io.File -import java.util.UUID import javax.inject.Inject -internal class ImageCompressor @Inject constructor(private val context: Context) { +internal class ImageCompressor @Inject constructor( + private val temporaryFileCreator: TemporaryFileCreator +) { suspend fun compress( imageFile: File, desiredWidth: Int, @@ -45,7 +46,7 @@ internal class ImageCompressor @Inject constructor(private val context: Context) } } ?: return@withContext imageFile - val destinationFile = createDestinationFile() + val destinationFile = temporaryFileCreator.create() runCatching { destinationFile.outputStream().use { @@ -53,7 +54,7 @@ internal class ImageCompressor @Inject constructor(private val context: Context) } } - return@withContext destinationFile + destinationFile } } @@ -64,16 +65,16 @@ internal class ImageCompressor @Inject constructor(private val context: Context) val orientation = exifInfo.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) val matrix = Matrix() when (orientation) { - ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f) - ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f) - ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f) + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f) + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f) ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f) - ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f) - ExifInterface.ORIENTATION_TRANSPOSE -> { + ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f) + ExifInterface.ORIENTATION_TRANSPOSE -> { matrix.preRotate(-90f) matrix.preScale(-1f, 1f) } - ExifInterface.ORIENTATION_TRANSVERSE -> { + ExifInterface.ORIENTATION_TRANSVERSE -> { matrix.preRotate(90f) matrix.preScale(-1f, 1f) } @@ -116,8 +117,4 @@ internal class ImageCompressor @Inject constructor(private val context: Context) null } } - - private fun createDestinationFile(): File { - return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 3b727690bfddccb54b07fc90c50740c1b90a54b9..06cbf1ba90241f7dd101e071ae92e3c0fbf0639b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -18,9 +18,12 @@ package org.matrix.android.sdk.internal.session.content import android.content.Context import android.graphics.BitmapFactory +import android.media.MediaMetadataRetriever +import androidx.core.net.toUri import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -41,12 +44,13 @@ import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker import org.matrix.android.sdk.internal.session.room.send.LocalEchoIdentifiers import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker +import org.matrix.android.sdk.internal.util.TemporaryFileCreator +import org.matrix.android.sdk.internal.util.toMatrixErrorStr import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber import java.io.File -import java.util.UUID import javax.inject.Inject private data class NewAttachmentAttributes( @@ -77,7 +81,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter @Inject lateinit var fileService: DefaultFileService @Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var imageCompressor: ImageCompressor + @Inject lateinit var videoCompressor: VideoCompressor @Inject lateinit var localEchoRepository: LocalEchoRepository + @Inject lateinit var temporaryFileCreator: TemporaryFileCreator override fun injectWith(injector: SessionComponent) { injector.inject(this) @@ -109,7 +115,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val attachment = params.attachment val filesToDelete = mutableListOf<File>() - try { + return try { val inputStream = context.contentResolver.openInputStream(attachment.queryUri) ?: return Result.success( WorkerParamsFactory.toData( @@ -120,7 +126,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) // always use a temporary file, it guaranties that we could report progress on upload and simplifies the flows - val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + val workingFile = temporaryFileCreator.create() .also { filesToDelete.add(it) } workingFile.outputStream().use { outputStream -> inputStream.use { inputStream -> @@ -128,8 +134,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } - val uploadThumbnailResult = dealWithThumbnail(params) - val progressListener = object : ProgressRequestBody.Listener { override fun onProgress(current: Long, total: Long) { notifyTracker(params) { @@ -144,7 +148,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null - return try { + try { val fileToUpload: File var newAttachmentAttributes = NewAttachmentAttributes( params.attachment.width?.toInt(), @@ -156,6 +160,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter // Do not compress gif && attachment.mimeType != MimeTypes.Gif && params.compressBeforeSending) { + notifyTracker(params) { contentUploadStateTracker.setCompressingImage(it) } + fileToUpload = imageCompressor.compress(workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) .also { compressedFile -> // Get new Bitmap size @@ -170,6 +176,48 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } .also { filesToDelete.add(it) } + } else if (attachment.type == ContentAttachmentData.Type.VIDEO + // Do not compress gif + && attachment.mimeType != MimeTypes.Gif + && params.compressBeforeSending) { + fileToUpload = videoCompressor.compress(workingFile, object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + notifyTracker(params) { contentUploadStateTracker.setCompressingVideo(it, progress.toFloat()) } + } + }) + .let { videoCompressionResult -> + when (videoCompressionResult) { + is VideoCompressionResult.Success -> { + val compressedFile = videoCompressionResult.compressedFile + var compressedWidth: Int? = null + var compressedHeight: Int? = null + + tryOrNull { + context.contentResolver.openFileDescriptor(compressedFile.toUri(), "r")?.use { pfd -> + MediaMetadataRetriever().let { + it.setDataSource(pfd.fileDescriptor) + compressedWidth = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() + compressedHeight = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() + } + } + } + + // Get new Video file size and dimensions + newAttachmentAttributes = newAttachmentAttributes.copy( + newFileSize = compressedFile.length(), + newWidth = compressedWidth ?: newAttachmentAttributes.newWidth, + newHeight = compressedHeight ?: newAttachmentAttributes.newHeight + ) + compressedFile + .also { filesToDelete.add(it) } + } + VideoCompressionResult.CompressionNotNeeded, + VideoCompressionResult.CompressionCancelled, + is VideoCompressionResult.CompressionFailed -> { + workingFile + } + } + } } else { fileToUpload = workingFile // Fix: OpenableColumns.SIZE may return -1 or 0 @@ -180,9 +228,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val encryptedFile: File? val contentUploadResponse = if (params.isEncrypted) { - Timber.v("## FileService: Encrypt file") - - encryptedFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + Timber.v("## Encrypt file") + encryptedFile = temporaryFileCreator.create() .also { filesToDelete.add(it) } uploadedFileEncryptedFileInfo = @@ -191,19 +238,25 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) } } - - Timber.v("## FileService: Uploading file") - - fileUploader - .uploadFile(encryptedFile, attachment.name, MimeTypes.OctetStream, progressListener) + Timber.v("## Uploading file") + fileUploader.uploadFile( + file = encryptedFile, + filename = null, + mimeType = MimeTypes.OctetStream, + progressListener = progressListener + ) } else { - Timber.v("## FileService: Clear file") + Timber.v("## Uploading clear file") encryptedFile = null - fileUploader - .uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener) + fileUploader.uploadFile( + file = fileToUpload, + filename = attachment.name, + mimeType = attachment.getSafeMimeType(), + progressListener = progressListener + ) } - Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") + Timber.v("## Update cache storage for ${contentUploadResponse.contentUri}") try { fileService.storeDataFor( mxcUrl = contentUploadResponse.contentUri, @@ -212,11 +265,13 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter originalFile = workingFile, encryptedFile = encryptedFile ) - Timber.v("## FileService: cache storage updated") + Timber.v("## cache storage updated") } catch (failure: Throwable) { - Timber.e(failure, "## FileService: Failed to update file cache") + Timber.e(failure, "## Failed to update file cache") } + val uploadThumbnailResult = dealWithThumbnail(params) + handleSuccess(params, contentUploadResponse.contentUri, uploadedFileEncryptedFileInfo, @@ -224,12 +279,12 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter uploadThumbnailResult?.uploadedThumbnailEncryptedFileInfo, newAttachmentAttributes) } catch (t: Throwable) { - Timber.e(t, "## FileService: ERROR ${t.localizedMessage}") + Timber.e(t, "## ERROR ${t.localizedMessage}") handleFailure(params, t) } } catch (e: Exception) { - Timber.e(e, "## FileService: ERROR") - return handleFailure(params, e) + Timber.e(e, "## ERROR") + handleFailure(params, e) } finally { // Delete all temporary files filesToDelete.forEach { @@ -260,19 +315,23 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter Timber.v("Encrypt thumbnail") notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType) - val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, - "thumb_${params.attachment.name}", - MimeTypes.OctetStream, - thumbnailProgressListener) + val contentUploadResponse = fileUploader.uploadByteArray( + byteArray = encryptionResult.encryptedByteArray, + filename = null, + mimeType = MimeTypes.OctetStream, + progressListener = thumbnailProgressListener + ) UploadThumbnailResult( contentUploadResponse.contentUri, encryptionResult.encryptedFileInfo ) } else { - val contentUploadResponse = fileUploader.uploadByteArray(thumbnailData.bytes, - "thumb_${params.attachment.name}", - thumbnailData.mimeType, - thumbnailProgressListener) + val contentUploadResponse = fileUploader.uploadByteArray( + byteArray = thumbnailData.bytes, + filename = "thumb_${params.attachment.name}", + mimeType = thumbnailData.mimeType, + progressListener = thumbnailProgressListener + ) UploadThumbnailResult( contentUploadResponse.contentUri, null @@ -291,7 +350,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter return Result.success( WorkerParamsFactory.toData( params.copy( - lastFailureMessage = failure.localizedMessage + lastFailureMessage = failure.toMatrixErrorStr() ) ) ) @@ -328,8 +387,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val messageContent: MessageContent? = event.asDomain().content.toModel() val updatedContent = when (messageContent) { is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes) - is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, - newAttachmentAttributes.newFileSize) + is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newAttachmentAttributes) is MessageFileContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) is MessageAudioContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) else -> messageContent @@ -351,7 +409,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter info = info?.copy( width = newAttachmentAttributes?.newWidth ?: info.width, height = newAttachmentAttributes?.newHeight ?: info.height, - size = newAttachmentAttributes?.newFileSize?.toInt() ?: info.size + size = newAttachmentAttributes?.newFileSize ?: info.size ) ) } @@ -360,14 +418,16 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter encryptedFileInfo: EncryptedFileInfo?, thumbnailUrl: String?, thumbnailEncryptedFileInfo: EncryptedFileInfo?, - size: Long): MessageVideoContent { + newAttachmentAttributes: NewAttachmentAttributes?): MessageVideoContent { return copy( url = if (encryptedFileInfo == null) url else null, encryptedFileInfo = encryptedFileInfo?.copy(url = url), videoInfo = videoInfo?.copy( thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null, thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = thumbnailUrl), - size = size + width = newAttachmentAttributes?.newWidth ?: videoInfo.width, + height = newAttachmentAttributes?.newHeight ?: videoInfo.height, + size = newAttachmentAttributes?.newFileSize ?: videoInfo.size ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressionResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..87d5c7e6a3ebbb2a8aec1dca8b81a170d08e0358 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressionResult.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.content + +import java.io.File + +internal sealed class VideoCompressionResult { + data class Success(val compressedFile: File) : VideoCompressionResult() + object CompressionNotNeeded : VideoCompressionResult() + object CompressionCancelled : VideoCompressionResult() + data class CompressionFailed(val failure: Throwable) : VideoCompressionResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt new file mode 100644 index 0000000000000000000000000000000000000000..05aaf4e9f16066686c43071f3195480b06fd5b1d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.content + +import com.otaliastudios.transcoder.Transcoder +import com.otaliastudios.transcoder.TranscoderListener +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.internal.util.TemporaryFileCreator +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +internal class VideoCompressor @Inject constructor( + private val temporaryFileCreator: TemporaryFileCreator +) { + + suspend fun compress(videoFile: File, + progressListener: ProgressListener?): VideoCompressionResult { + val destinationFile = temporaryFileCreator.create() + + val job = Job() + + Timber.d("Compressing: start") + progressListener?.onProgress(0, 100) + + var result: Int = -1 + var failure: Throwable? = null + Transcoder.into(destinationFile.path) + .addDataSource(videoFile.path) + .setListener(object : TranscoderListener { + override fun onTranscodeProgress(progress: Double) { + Timber.d("Compressing: $progress%") + progressListener?.onProgress((progress * 100).toInt(), 100) + } + + override fun onTranscodeCompleted(successCode: Int) { + Timber.d("Compressing: success: $successCode") + result = successCode + job.complete() + } + + override fun onTranscodeCanceled() { + Timber.d("Compressing: cancel") + job.cancel() + } + + override fun onTranscodeFailed(exception: Throwable) { + Timber.w(exception, "Compressing: failure") + failure = exception + job.completeExceptionally(exception) + } + }) + .transcode() + + job.join() + + // Note: job is also cancelled if completeExceptionally() was called + if (job.isCancelled) { + // Delete now the temporary file + deleteFile(destinationFile) + return when (val finalFailure = failure) { + null -> { + // We do not throw a CancellationException, because it's not critical, we will try to send the original file + // Anyway this should never occurs, since we never cancel the return value of transcode() + Timber.w("Compressing: A failure occurred") + VideoCompressionResult.CompressionCancelled + } + else -> { + // Compression failure can also be considered as not critical, but let the caller decide + Timber.w("Compressing: Job cancelled") + VideoCompressionResult.CompressionFailed(finalFailure) + } + } + } + + progressListener?.onProgress(100, 100) + + return when (result) { + Transcoder.SUCCESS_TRANSCODED -> { + VideoCompressionResult.Success(destinationFile) + } + Transcoder.SUCCESS_NOT_NEEDED -> { + // Delete now the temporary file + deleteFile(destinationFile) + VideoCompressionResult.CompressionNotNeeded + } + else -> { + // Should not happen... + // Delete now the temporary file + deleteFile(destinationFile) + Timber.w("Unknown result: $result") + VideoCompressionResult.CompressionFailed(IllegalStateException("Unknown result: $result")) + } + } + } + + private suspend fun deleteFile(file: File) { + withContext(Dispatchers.IO) { + file.delete() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt index f5391d6cdb113bcc3b3d1a244aabfca9d2eff3f4..475781ef017a4eda68f64f12ad6ec96620f074e7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt @@ -36,7 +36,7 @@ import org.matrix.android.sdk.internal.di.AuthenticatedIdentity import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.extensions.observeNotNull import org.matrix.android.sdk.internal.network.RetrofitFactory -import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.identity.data.IdentityStore import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask @@ -51,6 +51,7 @@ import org.matrix.android.sdk.internal.util.ensureProtocol import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.Session import timber.log.Timber import javax.inject.Inject import javax.net.ssl.HttpsURLConnection @@ -86,7 +87,7 @@ internal class DefaultIdentityService @Inject constructor( private val listeners = mutableSetOf<IdentityServiceListener>() - override fun onSessionStarted() { + override fun onSessionStarted(session: Session) { lifecycleRegistry.currentState = Lifecycle.State.STARTED // Observe the account data change accountDataDataSource @@ -111,7 +112,7 @@ internal class DefaultIdentityService @Inject constructor( } } - override fun onSessionStopped() { + override fun onSessionStopped(session: Session) { lifecycleRegistry.currentState = Lifecycle.State.DESTROYED } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt index 1671859585f1d35cff1f218d1df1d69dae8ff8ae..9d990d4d8f9340a91bbb07dd8a792c8eb216a4ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt @@ -16,9 +16,9 @@ package org.matrix.android.sdk.internal.session.identity +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse -import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -52,5 +52,5 @@ internal interface IdentityAuthAPI { * The request body is the same as the values returned by /openid/request_token in the Client-Server API. */ @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register") - suspend fun register(@Body openIdToken: RequestOpenIdTokenResponse): IdentityRegisterResponse + suspend fun register(@Body openIdToken: OpenIdToken): IdentityRegisterResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt index 4f6e906766801806c67a0d6cc154b1d25fff4828..114695062c867b107589a2bf91c16bc6c045f05a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -117,7 +117,7 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( return withOlmUtility { olmUtility -> threePids.map { threePid -> base64ToBase64Url( - olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT) + olmUtility.sha256(threePid.value.lowercase(Locale.ROOT) + " " + threePid.toMedium() + " " + pepper) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt index 8cc854bd940daa821d0f785db5fb31375aafa547..1800d0eebe2cc76280ac6a9ba58b4f710188500c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt @@ -16,16 +16,16 @@ package org.matrix.android.sdk.internal.session.identity +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse -import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject internal interface IdentityRegisterTask : Task<IdentityRegisterTask.Params, IdentityRegisterResponse> { data class Params( val identityAuthAPI: IdentityAuthAPI, - val openIdTokenResponse: RequestOpenIdTokenResponse + val openIdToken: OpenIdToken ) } @@ -33,7 +33,7 @@ internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegis override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse { return executeRequest(null) { - params.identityAuthAPI.register(params.openIdTokenResponse) + params.identityAuthAPI.register(params.openIdToken) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt index e34615d269dd8e097115f45ba8c3893908dace55..3df9a00cc15fa494a95945c5ccd8b85889a98740 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService @@ -29,7 +30,7 @@ import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.internal.database.model.WellknownIntegrationManagerConfigEntity import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.extensions.observeNotNull -import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent @@ -77,7 +78,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri currentConfigs.add(defaultConfig) } - override fun onSessionStarted() { + override fun onSessionStarted(session: Session) { lifecycleRegistry.currentState = Lifecycle.State.STARTED observeWellknownConfig() accountDataDataSource @@ -105,7 +106,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri } } - override fun onSessionStopped() { + override fun onSessionStopped(session: Session) { lifecycleRegistry.currentState = Lifecycle.State.DESTROYED } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt new file mode 100644 index 0000000000000000000000000000000000000000..b90a2435f7d1c5ff6378ffd1621febcf48d9e9ae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.openid + +import org.matrix.android.sdk.api.session.openid.OpenIdService +import org.matrix.android.sdk.api.session.openid.OpenIdToken +import javax.inject.Inject + +internal class DefaultOpenIdService @Inject constructor(private val getOpenIdTokenTask: GetOpenIdTokenTask): OpenIdService { + + override suspend fun getOpenIdToken(): OpenIdToken { + return getOpenIdTokenTask.execute(Unit) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt index 8481a6ab93d6eaec9e1af4b454c8f86746b6d11d..a6ad025b8d769301d58ede89a545daf15410c2ec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt @@ -16,20 +16,21 @@ package org.matrix.android.sdk.internal.session.openid +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -internal interface GetOpenIdTokenTask : Task<Unit, RequestOpenIdTokenResponse> +internal interface GetOpenIdTokenTask : Task<Unit, OpenIdToken> internal class DefaultGetOpenIdTokenTask @Inject constructor( @UserId private val userId: String, private val openIdAPI: OpenIdAPI, private val globalErrorReceiver: GlobalErrorReceiver) : GetOpenIdTokenTask { - override suspend fun execute(params: Unit): RequestOpenIdTokenResponse { + override suspend fun execute(params: Unit): OpenIdToken { return executeRequest(globalErrorReceiver) { openIdAPI.openIdToken(userId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt index ed090b845d02888f912ede1657b20bab8ce540c9..eb8c841d5771afb2c656d3a962842429628e3508 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.openid +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.network.NetworkConstants import retrofit2.http.Body @@ -34,5 +35,5 @@ internal interface OpenIdAPI { */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") suspend fun openIdToken(@Path("userId") userId: String, - @Body body: JsonDict = emptyMap()): RequestOpenIdTokenResponse + @Body body: JsonDict = emptyMap()): OpenIdToken } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt index 71a6e224bfdbe6ac837573f17f8517c4a9ec9166..970752449aa04e00960327939be32ba09d4f2225 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt @@ -18,19 +18,13 @@ package org.matrix.android.sdk.internal.session.permalinks import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE -import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams -import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.room.RoomGetter -import java.net.URLEncoder import javax.inject.Inject -import javax.inject.Provider internal class PermalinkFactory @Inject constructor( @UserId private val userId: String, - // Use a provider to fix circular Dagger dependency - private val roomGetterProvider: Provider<RoomGetter> + private val viaParameterFinder: ViaParameterFinder ) { fun createPermalink(event: Event): String? { @@ -50,12 +44,12 @@ internal class PermalinkFactory @Inject constructor( return if (roomId.isEmpty()) { null } else { - MATRIX_TO_URL_BASE + escape(roomId) + computeViaParams(userId, roomId) + MATRIX_TO_URL_BASE + escape(roomId) + viaParameterFinder.computeViaParams(userId, roomId) } } fun createPermalink(roomId: String, eventId: String): String { - return MATRIX_TO_URL_BASE + escape(roomId) + "/" + escape(eventId) + computeViaParams(userId, roomId) + return MATRIX_TO_URL_BASE + escape(roomId) + "/" + escape(eventId) + viaParameterFinder.computeViaParams(userId, roomId) } fun getLinkedId(url: String): String? { @@ -66,25 +60,6 @@ internal class PermalinkFactory @Inject constructor( } else null } - /** - * Compute the via parameters. - * Take up to 3 homeserver domains, taking the most representative one regarding room members and including the - * current user one. - */ - private fun computeViaParams(userId: String, roomId: String): String { - val userHomeserver = userId.substringAfter(":") - return getUserIdsOfJoinedMembers(roomId) - .map { it.substringAfter(":") } - .groupBy { it } - .mapValues { it.value.size } - .toMutableMap() - // Ensure the user homeserver will be included - .apply { this[userHomeserver] = Int.MAX_VALUE } - .let { map -> map.keys.sortedByDescending { map[it] } } - .take(3) - .joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") } - } - /** * Escape '/' in id, because it is used as a separator * @@ -104,15 +79,4 @@ internal class PermalinkFactory @Inject constructor( private fun unescape(id: String): String { return id.replace("%2F", "/") } - - /** - * Get a set of userIds of joined members of a room - */ - private fun getUserIdsOfJoinedMembers(roomId: String): Set<String> { - return roomGetterProvider.get().getRoom(roomId) - ?.getRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.JOIN) }) - ?.map { it.userId } - .orEmpty() - .toSet() - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt new file mode 100644 index 0000000000000000000000000000000000000000..0da60e9ba2affa030270720565911884ab7ab36d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.permalinks + +import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.RoomGetter +import java.net.URLEncoder +import javax.inject.Inject +import javax.inject.Provider + +internal class ViaParameterFinder @Inject constructor( + @UserId private val userId: String, + private val roomGetterProvider: Provider<RoomGetter> +) { + + fun computeViaParams(roomId: String, max: Int): List<String> { + return computeViaParams(userId, roomId, max) + } + + /** + * Compute the via parameters. + * Take up to 3 homeserver domains, taking the most representative one regarding room members and including the + * current user one. + */ + fun computeViaParams(userId: String, roomId: String): String { + return computeViaParams(userId, roomId, 3) + .joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") } + } + + fun computeViaParams(userId: String, roomId: String, max: Int): List<String> { + val userHomeserver = userId.substringAfter(":") + return getUserIdsOfJoinedMembers(roomId) + .map { it.substringAfter(":") } + .groupBy { it } + .mapValues { it.value.size } + .toMutableMap() + // Ensure the user homeserver will be included + .apply { this[userHomeserver] = Int.MAX_VALUE } + .let { map -> map.keys.sortedByDescending { map[it] } } + .take(max) + } + + /** + * Get a set of userIds of joined members of a room + */ + private fun getUserIdsOfJoinedMembers(roomId: String): Set<String> { + return roomGetterProvider.get().getRoom(roomId) + ?.getRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.JOIN) }) + ?.map { it.userId } + .orEmpty() + .toSet() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt index 5113b821e8726764e8530b112f342b7c5edf0a24..485a4973cab0f838e566ff57f3ee4092bd0ad769 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt @@ -33,6 +33,7 @@ internal interface ProfileAPI { * Get the combined profile information for this user. * This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers. * This API may return keys which are not limited to displayname or avatar_url. + * If server is configured as limit_profile_requests_to_users_who_share_rooms: true then response can be HTTP 403. * @param userId the user id to fetch profile info */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 1d8eb6c95ea7d2628b15ed3ecb86e9f8517eb10b..a5e066dae80331f7c8601fcde66af626d69bedab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService import org.matrix.android.sdk.api.session.room.read.ReadService @@ -36,11 +37,14 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService import org.matrix.android.sdk.api.session.search.SearchResult +import org.matrix.android.sdk.api.session.space.Space import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.search.SearchTask +import org.matrix.android.sdk.internal.session.space.DefaultSpace import org.matrix.android.sdk.internal.util.awaitCallback import java.security.InvalidParameterException import javax.inject.Inject @@ -63,6 +67,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val roomMembersService: MembershipService, private val roomPushRuleService: RoomPushRuleService, private val sendStateTask: SendStateTask, + private val viaParameterFinder: ViaParameterFinder, private val searchTask: SearchTask) : Room, TimelineService by timelineService, @@ -148,4 +153,9 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, ) ) } + + override fun asSpace(): Space? { + if (roomSummary()?.roomType != RoomType.SPACE) return null + return DefaultSpace(this, roomSummaryDataSource, viaParameterFinder) + } } 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 bd63ba480eb8e3c29f6491dda18ed54b29dff56a..d9fe1288e29d0a538321ed390d9125773d3945e2 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 @@ -20,19 +20,19 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.RoomService +import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams -import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult +import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary 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.peeking.PeekResult import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.mapper.asDomain @@ -50,8 +50,6 @@ import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.fetchCopied import javax.inject.Inject @@ -67,16 +65,11 @@ internal class DefaultRoomService @Inject constructor( private val peekRoomTask: PeekRoomTask, private val roomGetter: RoomGetter, private val roomSummaryDataSource: RoomSummaryDataSource, - private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, - private val taskExecutor: TaskExecutor + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource ) : RoomService { - override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable { - return createRoomTask - .configureWith(createRoomParams) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun createRoom(createRoomParams: CreateRoomParams): String { + return createRoomTask.executeRetry(createRoomParams, 3) } override fun getRoom(roomId: String): Room? { @@ -99,14 +92,14 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getRoomSummariesLive(queryParams) } - override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config) + override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config, sortOrder: RoomSortOrder) : LiveData<PagedList<RoomSummary>> { - return roomSummaryDataSource.getSortedPagedRoomSummariesLive(queryParams, pagedListConfig) + return roomSummaryDataSource.getSortedPagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder) } - override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config) - : UpdatableFilterLivePageResult { - return roomSummaryDataSource.getFilteredPagedRoomSummariesLive(queryParams, pagedListConfig) + override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config, sortOrder: RoomSortOrder) + : UpdatableLivePageResult { + return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder) } override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount { @@ -121,34 +114,20 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getBreadcrumbsLive(queryParams) } - override fun onRoomDisplayed(roomId: String): Cancelable { - return updateBreadcrumbsTask - .configureWith(UpdateBreadcrumbsTask.Params(roomId)) - .executeBy(taskExecutor) + override suspend fun onRoomDisplayed(roomId: String) { + updateBreadcrumbsTask.execute(UpdateBreadcrumbsTask.Params(roomId)) } - override fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable { - return joinRoomTask - .configureWith(JoinRoomTask.Params(roomIdOrAlias, reason, viaServers)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List<String>) { + joinRoomTask.execute(JoinRoomTask.Params(roomIdOrAlias, reason, viaServers)) } - override fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable { - return markAllRoomsReadTask - .configureWith(MarkAllRoomsReadTask.Params(roomIds)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun markAllAsRead(roomIds: List<String>) { + markAllRoomsReadTask.execute(MarkAllRoomsReadTask.Params(roomIds)) } - override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable { - return roomIdByAliasTask - .configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean): Optional<RoomAliasDescription> { + return roomIdByAliasTask.execute(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) } override suspend fun deleteRoomAlias(roomAlias: String) { @@ -179,19 +158,25 @@ internal class DefaultRoomService @Inject constructor( } } - override fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>) { - resolveRoomStateTask - .configureWith(ResolveRoomStateTask.Params(roomId)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun getRoomState(roomId: String): List<Event> { + return resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomId)) } - override fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>) { - peekRoomTask - .configureWith(PeekRoomTask.Params(roomIdOrAlias)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun peekRoom(roomIdOrAlias: String): PeekResult { + return peekRoomTask.execute(PeekRoomTask.Params(roomIdOrAlias)) + } + + override fun getFlattenRoomSummaryChildrenOf(spaceId: String?, memberships: List<Membership>): List<RoomSummary> { + if (spaceId == null) { + return roomSummaryDataSource.getFlattenOrphanRooms() + } + return roomSummaryDataSource.getAllRoomSummaryChildOf(spaceId, memberships) + } + + override fun getFlattenRoomSummaryChildrenOfLive(spaceId: String?, memberships: List<Membership>): LiveData<List<RoomSummary>> { + if (spaceId == null) { + return roomSummaryDataSource.getFlattenOrphanRoomsLive() + } + return roomSummaryDataSource.getAllRoomSummaryChildOfLive(spaceId, memberships) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index 90640b47000dbfa674b9a0d793c1ae6ed39a35af..3f743c29229d4977adc881ca4ed0f80ba068443b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder import org.matrix.android.sdk.internal.session.room.alias.DefaultAliasService import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService @@ -60,6 +61,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: private val membershipServiceFactory: DefaultMembershipService.Factory, private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory, private val sendStateTask: SendStateTask, + private val viaParameterFinder: ViaParameterFinder, private val searchTask: SearchTask) : RoomFactory { @@ -83,7 +85,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: roomMembersService = membershipServiceFactory.create(roomId), roomPushRuleService = roomPushRuleServiceFactory.create(roomId), sendStateTask = sendStateTask, - searchTask = searchTask + searchTask = searchTask, + viaParameterFinder = viaParameterFinder ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 5133f72932b2fe3d0ffe05009bb0cc9a2eea7c7a..8f3445bec384ea67ad43366f10dd1e968ec2c8dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -24,6 +24,7 @@ import org.commonmark.renderer.html.HtmlRenderer import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.RoomDirectoryService import org.matrix.android.sdk.api.session.room.RoomService +import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.internal.session.DefaultFileService import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.directory.DirectoryAPI @@ -89,6 +90,7 @@ import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask +import org.matrix.android.sdk.internal.session.space.DefaultSpaceService import retrofit2.Retrofit @Module @@ -135,6 +137,9 @@ internal abstract class RoomModule { @Binds abstract fun bindRoomService(service: DefaultRoomService): RoomService + @Binds + abstract fun bindSpaceService(service: DefaultSpaceService): SpaceService + @Binds abstract fun bindRoomDirectoryService(service: DefaultRoomDirectoryService): RoomDirectoryService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt new file mode 100644 index 0000000000000000000000000000000000000000..fed3ff542b7e6baf4de0dcebe89f6aaa036fa475 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.session.space.Space +import javax.inject.Inject + +internal interface SpaceGetter { + fun get(spaceId: String): Space? +} + +internal class DefaultSpaceGetter @Inject constructor( + private val roomGetter: RoomGetter +) : SpaceGetter { + + override fun get(spaceId: String): Space? { + return roomGetter.getRoom(spaceId)?.asSpace() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt index 9faf50dd8b70b2a6fadab5387ef51644f578c853..b39cbaa5825c69ecfda9656115e8827542626fa8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt @@ -36,7 +36,11 @@ internal class RoomAliasAvailabilityChecker @Inject constructor( @Throws(RoomAliasError::class) suspend fun check(aliasLocalPart: String?) { if (aliasLocalPart.isNullOrEmpty()) { - throw RoomAliasError.AliasEmpty + // don't check empty or not provided alias + return + } + if (aliasLocalPart.isBlank()) { + throw RoomAliasError.AliasIsBlank } // Check alias availability val fullAlias = aliasLocalPart.toFullLocalAlias(userId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt index 13d403e2e4bee4588988a6ebc70790d78d4e81aa..69352688e3b74382804011f01d9c49fb745ad8b2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt @@ -111,5 +111,12 @@ internal data class CreateRoomBody( * The power level content to override in the default power level event */ @Json(name = "power_level_content_override") - val powerLevelContentOverride: PowerLevelsContent? + val powerLevelContentOverride: PowerLevelsContent?, + + /** + * The room version to set for the room. If not provided, the homeserver is to use its configured default. + * If provided, the homeserver will return a 400 error with the errcode M_UNSUPPORTED_ROOM_VERSION if it does not support the room version. + */ + @Json(name = "room_version") + val roomVersion: String? ) 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 5e823fc87fe015d89c0eadb0f980c64c77a042c5..018b8653886ac861ffc7ca791cdda340af72af20 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 @@ -20,13 +20,19 @@ 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.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.util.MimeTypes 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.di.UserId 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 @@ -43,6 +49,8 @@ internal class CreateRoomBodyBuilder @Inject constructor( private val deviceListManager: DeviceListManager, private val identityStore: IdentityStore, private val fileUploader: FileUploader, + @UserId + private val userId: String, @AuthenticatedIdentity private val accessTokenProvider: AccessTokenProvider ) { @@ -68,10 +76,17 @@ internal class CreateRoomBodyBuilder @Inject constructor( } } + if (params.joinRuleRestricted != null) { + params.roomVersion = "org.matrix.msc3083" + params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED + params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden + } val initialStates = listOfNotNull( buildEncryptionWithAlgorithmEvent(params), buildHistoryVisibilityEvent(params), - buildAvatarEvent(params) + buildAvatarEvent(params), + buildGuestAccess(params), + buildJoinRulesRestricted(params) ) .takeIf { it.isNotEmpty() } @@ -80,13 +95,15 @@ internal class CreateRoomBodyBuilder @Inject constructor( roomAliasName = params.roomAliasName, name = params.name, topic = params.topic, - invitedUserIds = params.invitedUserIds, + invitedUserIds = params.invitedUserIds.filter { it != userId }, invite3pids = invite3pids, creationContent = params.creationContent.takeIf { it.isNotEmpty() }, initialStates = initialStates, preset = params.preset, isDirect = params.isDirect, - powerLevelContentOverride = params.powerLevelContentOverride + powerLevelContentOverride = params.powerLevelContentOverride, + roomVersion = params.roomVersion + ) } @@ -120,6 +137,31 @@ internal class CreateRoomBodyBuilder @Inject constructor( } } + private fun buildGuestAccess(params: CreateRoomParams): Event? { + return params.guestAccess + ?.let { + Event( + type = EventType.STATE_ROOM_GUEST_ACCESS, + stateKey = "", + content = mapOf("guest_access" to it.value) + ) + } + } + + private fun buildJoinRulesRestricted(params: CreateRoomParams): Event? { + return params.joinRuleRestricted + ?.let { allowList -> + Event( + type = EventType.STATE_ROOM_JOIN_RULES, + stateKey = "", + content = RoomJoinRulesContent( + _joinRules = RoomJoinRules.RESTRICTED.value, + allowList = allowList + ).toContent() + ) + } + } + /** * Add the crypto algorithm to the room creation parameters. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt index bafe2b90ae0b197773436d386d29ff56b8083080..de6a71e58157e947ac381dccc1442b6641374806 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -102,7 +102,7 @@ internal class DefaultCreateRoomTask @Inject constructor( .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) } } catch (exception: TimeoutCancellationException) { - throw CreateRoomFailure.CreatedWithTimeout + throw CreateRoomFailure.CreatedWithTimeout(roomId) } Realm.getInstance(realmConfiguration).executeTransactionAsync { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt index 5b211c505fe5b48a9bd2d665db1b52c82880cf1a..c6f4bbb4e1a9fdd7cadb464acfd87ea4a42d5a31 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt @@ -23,11 +23,14 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent 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.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask @@ -100,7 +103,9 @@ internal class DefaultPeekRoomTask @Inject constructor( name = publicRepoResult.name, topic = publicRepoResult.topic, numJoinedMembers = publicRepoResult.numJoinedMembers, - viaServers = serverList + viaServers = serverList, + roomType = null, // would be nice to get that from directory... + someMembers = null ) } @@ -125,11 +130,25 @@ internal class DefaultPeekRoomTask @Inject constructor( ?.let { it.content?.toModel<RoomCanonicalAliasContent>()?.canonicalAlias } // not sure if it's the right way to do that :/ - val memberCount = stateEvents + val membersEvent = stateEvents .filter { it.type == EventType.STATE_ROOM_MEMBER && it.stateKey?.isNotEmpty() == true } + + val memberCount = membersEvent .distinctBy { it.stateKey } .count() + val someMembers = membersEvent.mapNotNull { ev -> + ev.content?.toModel<RoomMemberContent>()?.let { + MatrixItem.UserItem(ev.stateKey ?: "", it.displayName, it.avatarUrl) + } + } + + val roomType = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_CREATE } + ?.content + ?.toModel<RoomCreateContent>() + ?.type + return PeekResult.Success( roomId = roomId, alias = alias, @@ -137,7 +156,9 @@ internal class DefaultPeekRoomTask @Inject constructor( name = name, topic = topic, numJoinedMembers = memberCount, - viaServers = serverList + roomType = roomType, + viaServers = serverList, + someMembers = someMembers ) } catch (failure: Throwable) { // Would be M_FORBIDDEN if cannot peek :/ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index 5fe06287d277aea08a8d68a1ec3ecfd8a20f1d1c..a666d40fc319d96d1d3b0e851314358bf83c58fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -99,6 +99,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: entity.age = editedEventEntity.age entity.originServerTs = editedEventEntity.originServerTs entity.sendState = editedEventEntity.sendState + entity.sendStateDetails = editedEventEntity.sendStateDetails } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..2efea7f118f91f8c21e9ebe08adf108b93bcd02c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.relationship + +import io.realm.Realm +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.space.model.SpaceChildContent +import org.matrix.android.sdk.api.session.space.model.SpaceParentContent +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.query.whereType + +/** + * Relationship between rooms and spaces + * The intention is that rooms and spaces form a hierarchy, which clients can use to structure the user's room list into a tree view. + * The parent/child relationship can be expressed in one of two ways: + * - The admins of a space can advertise rooms and subspaces for their space by setting m.space.child state events. + * The state_key is the ID of a child room or space, and the content should contain a via key which gives + * a list of candidate servers that can be used to join the room. present: true key is included to distinguish from a deleted state event. + * + * - Separately, rooms can claim parents via the m.room.parent state event. + */ +internal class RoomChildRelationInfo( + private val realm: Realm, + private val roomId: String +) { + + data class SpaceChildInfo( + val roomId: String, + val order: String?, + val autoJoin: Boolean, + val viaServers: List<String> + ) + + data class SpaceParentInfo( + val roomId: String, + val canonical: Boolean, + val viaServers: List<String>, + val stateEventSender: String + ) + + /** + * Gets the ordered list of valid child description. + */ + fun getDirectChildrenDescriptions(): List<SpaceChildInfo> { + return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD) + .findAll() +// .also { +// Timber.v("## Space: Found ${it.count()} m.space.child state events for $roomId") +// } + .mapNotNull { + ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.let { scc -> +// Timber.v("## Space child desc state event $scc") + // Children where via is not present are ignored. + scc.via?.let { via -> + SpaceChildInfo( + roomId = it.stateKey, + order = scc.validOrder(), + autoJoin = scc.autoJoin ?: false, + viaServers = via + ) + } + } + } + .sortedBy { it.order } + } + + fun getParentDescriptions(): List<SpaceParentInfo> { + return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_PARENT) + .findAll() +// .also { +// Timber.v("## Space: Found ${it.count()} m.space.parent state events for $roomId") +// } + .mapNotNull { + ContentMapper.map(it.root?.content).toModel<SpaceParentContent>()?.let { scc -> +// Timber.v("## Space parent desc state event $scc") + // Parent where via is not present are ignored. + scc.via?.let { via -> + SpaceParentInfo( + roomId = it.stateKey, + canonical = scc.canonical ?: false, + viaServers = via, + stateEventSender = it.root?.sender ?: "" + ) + } + } + } + } +} 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 26a87557ff490686964c163542bf0965c92e2768..449189e6b56cb0ef723e5f8bf8e67511b24c4f86 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 @@ -109,14 +109,6 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } - override fun sendMedias(attachments: List<ContentAttachmentData>, - compressBeforeSending: Boolean, - roomIds: Set<String>): Cancelable { - return attachments.mapTo(CancelableBag()) { - sendMedia(it, compressBeforeSending, roomIds) - } - } - override fun redactEvent(event: Event, reason: String?): Cancelable { // TODO manage media/attachements? val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) @@ -149,7 +141,7 @@ internal class DefaultSendService @AssistedInject constructor( is MessageImageContent -> { // The image has not yet been sent val attachmentData = ContentAttachmentData( - size = messageContent.info!!.size.toLong(), + size = messageContent.info!!.size, mimeType = messageContent.info.mimeType!!, width = messageContent.info.width.toLong(), height = messageContent.info.height.toLong(), @@ -240,6 +232,14 @@ internal class DefaultSendService @AssistedInject constructor( } } + override fun sendMedias(attachments: List<ContentAttachmentData>, + compressBeforeSending: Boolean, + roomIds: Set<String>): Cancelable { + return attachments.mapTo(CancelableBag()) { + sendMedia(it, compressBeforeSending, roomIds) + } + } + override fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set<String>): Cancelable { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 432a4af062046896c48ea15929b4e971ad67a1f9..c1ad6205c38c383b7d4d76e3c498605e524211fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -244,7 +244,7 @@ internal class LocalEchoEventFactory @Inject constructor( mimeType = attachment.getSafeMimeType(), width = width?.toInt() ?: 0, height = height?.toInt() ?: 0, - size = attachment.size.toInt() + size = attachment.size ), url = attachment.queryUri.toString() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt index 70245cbd5e49eead24cd825787ecc5bad2588016..e98e5646afe38a5eeba1c4e68ac75db85ea63098 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -87,7 +87,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } } - fun updateSendState(eventId: String, roomId: String?, sendState: SendState) { + fun updateSendState(eventId: String, roomId: String?, sendState: SendState, sendStateDetails: String? = null) { Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}") timelineInput.onLocalEchoUpdated(roomId = roomId ?: "", eventId = eventId, sendState = sendState) updateEchoAsync(eventId) { realm, sendingEventEntity -> @@ -96,6 +96,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } else { sendingEventEntity.sendState = sendState } + sendingEventEntity.sendStateDetails = sendStateDetails roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId) } } @@ -161,6 +162,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val timelineEvents = TimelineEventEntity.where(realm, roomId, eventIds).findAll() timelineEvents.forEach { it.root?.sendState = sendState + it.root?.sendStateDetails = null } roomSummaryUpdater.updateSendingInformation(realm, roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index bc307bc74f7fed0bcd166ea0310583ce2e31177a..e889f1a61ba00d6a47f13a8d36dbb00eea8bcfa4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -55,7 +55,12 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo override fun doOnError(params: Params): Result { params.localEchoIds.forEach { localEchoIds -> - localEchoRepository.updateSendState(localEchoIds.eventId, localEchoIds.roomId, SendState.UNDELIVERED) + localEchoRepository.updateSendState( + eventId = localEchoIds.eventId, + roomId = localEchoIds.roomId, + sendState = SendState.UNDELIVERED, + sendStateDetails = params.lastFailureMessage + ) } return super.doOnError(params) 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 d55dce57afaadb87a322ba60772616008c7d482f..cd7911910dd8eda81a4c13038f49cd450fd69d8c 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 @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.util.toMatrixErrorStr import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import timber.log.Timber @@ -77,7 +78,12 @@ internal class SendEventWorker(context: Context, } if (params.lastFailureMessage != null) { - localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED) + localEchoRepository.updateSendState( + eventId = event.eventId, + roomId = event.roomId, + sendState = SendState.UNDELIVERED, + sendStateDetails = params.lastFailureMessage + ) // Transmit the error return Result.success(inputData) .also { Timber.e("Work cancelled due to input error from parent") } @@ -90,7 +96,12 @@ internal class SendEventWorker(context: Context, } catch (exception: Throwable) { if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) { Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}") - localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED) + localEchoRepository.updateSendState( + eventId = event.eventId, + roomId = event.roomId, + sendState = SendState.UNDELIVERED, + sendStateDetails = exception.toMatrixErrorStr() + ) Result.success() } else { Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt index 8bafa5f882c7f7d32c41664b3554cf15ca8113a0..cd5bf575dba4d66837949d8b9fa92bbba321db5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.room.send.queue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.SessionLifecycleObserver internal interface EventSenderProcessor: SessionLifecycleObserver { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt index a5c09f5ff69aa80ab593fa0bb49479e0efd6b332..80bfd02b0eef8bcec3f29cde2d5d2f2b4451c673 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.getRetryDelay +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.util.Cancelable @@ -72,7 +73,7 @@ internal class EventSenderProcessorCoroutine @Inject constructor( */ private val cancelableBag = ConcurrentHashMap<String, Cancelable>() - override fun onSessionStarted() { + override fun onSessionStarted(session: Session) { // We should check for sending events not handled because app was killed // But we should be careful of only took those that was submitted to us, because if it's // for example it's a media event it is handled by some worker and he will handle it diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt index b79a86dd7e0ce8392171d1aa560112b5587b9bbc..9db7cc90394fbb7378231dc963314e58d7ddac30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.isTokenError +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.sync.SyncState @@ -64,11 +65,11 @@ internal class EventSenderProcessorThread @Inject constructor( memento.unTrack(task) } - override fun onSessionStarted() { + override fun onSessionStarted(session: Session) { start() } - override fun onSessionStopped() { + override fun onSessionStopped(session: Session) { interrupt() } 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 615bc99096d63055e1df98b7cea56f9279a998bc..ff2afb5d61aff480092658ea0c8d9900e0de1f8b 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 @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.content.FileUploader +import java.lang.UnsupportedOperationException internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, private val stateEventDataSource: StateEventDataSource, @@ -73,7 +74,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private eventType = eventType, body = body.toSafeJson(eventType) ) - sendStateTask.execute(params) + sendStateTask.executeRetry(params, 3) } private fun JsonDict.toSafeJson(eventType: String): JsonDict { @@ -127,6 +128,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) { if (joinRules != null) { + if (joinRules == RoomJoinRules.RESTRICTED) throw UnsupportedOperationException("No yet supported") sendStateEvent( eventType = EventType.STATE_ROOM_JOIN_RULES, body = mapOf("join_rule" to joinRules), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt index a97709e38ba2d1d3e0b83a99503ab93c0e99cb4b..197b4f8688c7d38cf244785b21bb39d96bd40df3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt @@ -21,22 +21,21 @@ import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent -import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.util.JsonDict @JsonClass(generateAdapter = true) internal data class SerializablePowerLevelsContent( - @Json(name = "ban") val ban: Int = Role.Moderator.value, - @Json(name = "kick") val kick: Int = Role.Moderator.value, - @Json(name = "invite") val invite: Int = Role.Moderator.value, - @Json(name = "redact") val redact: Int = Role.Moderator.value, - @Json(name = "events_default") val eventsDefault: Int = Role.Default.value, - @Json(name = "events") val events: Map<String, Int> = emptyMap(), - @Json(name = "users_default") val usersDefault: Int = Role.Default.value, - @Json(name = "users") val users: Map<String, Int> = emptyMap(), - @Json(name = "state_default") val stateDefault: Int = Role.Moderator.value, + @Json(name = "ban") val ban: Int?, + @Json(name = "kick") val kick: Int?, + @Json(name = "invite") val invite: Int?, + @Json(name = "redact") val redact: Int?, + @Json(name = "events_default") val eventsDefault: Int?, + @Json(name = "events") val events: Map<String, Int>?, + @Json(name = "users_default") val usersDefault: Int?, + @Json(name = "users") val users: Map<String, Int>?, + @Json(name = "state_default") val stateDefault: Int?, // `Int` is the diff here (instead of `Any`) - @Json(name = "notifications") val notifications: Map<String, Int> = emptyMap() + @Json(name = "notifications") val notifications: Map<String, Int>? ) internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict { @@ -52,7 +51,7 @@ internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict { usersDefault = content.usersDefault, users = content.users, stateDefault = content.stateDefault, - notifications = content.notifications.mapValues { content.notificationLevel(it.key) } + notifications = content.notifications?.mapValues { content.notificationLevel(it.key) } ) } ?.toContent() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/GraphUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/GraphUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..b7e6548b54f8855a7e349b7243478b2e84006f9f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/GraphUtils.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.summary + +import java.util.LinkedList + +data class GraphNode( + val name: String +) + +data class GraphEdge( + val source: GraphNode, + val destination: GraphNode +) + +class Graph { + + private val adjacencyList: HashMap<GraphNode, ArrayList<GraphEdge>> = HashMap() + + fun getOrCreateNode(name: String): GraphNode { + return adjacencyList.entries.firstOrNull { it.key.name == name }?.key + ?: GraphNode(name).also { + adjacencyList[it] = ArrayList() + } + } + + fun addEdge(sourceName: String, destinationName: String) { + val source = getOrCreateNode(sourceName) + val destination = getOrCreateNode(destinationName) + adjacencyList.getOrPut(source) { ArrayList() }.add( + GraphEdge(source, destination) + ) + } + + fun addEdge(source: GraphNode, destination: GraphNode) { + adjacencyList.getOrPut(source) { ArrayList() }.add( + GraphEdge(source, destination) + ) + } + + fun edgesOf(node: GraphNode): List<GraphEdge> { + return adjacencyList[node]?.toList() ?: emptyList() + } + + fun withoutEdges(edgesToPrune: List<GraphEdge>): Graph { + val output = Graph() + this.adjacencyList.forEach { (vertex, edges) -> + output.getOrCreateNode(vertex.name) + edges.forEach { + if (!edgesToPrune.contains(it)) { + // add it + output.addEdge(it.source, it.destination) + } + } + } + return output + } + + /** + * Depending on the chosen starting point the background edge might change + */ + fun findBackwardEdges(startFrom: GraphNode? = null): List<GraphEdge> { + val backwardEdges = mutableSetOf<GraphEdge>() + val visited = mutableMapOf<GraphNode, Int>() + val notVisited = -1 + val inPath = 0 + val completed = 1 + adjacencyList.keys.forEach { + visited[it] = notVisited + } + val stack = LinkedList<GraphNode>() + + (startFrom ?: adjacencyList.entries.firstOrNull { visited[it.key] == notVisited }?.key) + ?.let { + stack.push(it) + visited[it] = inPath + } + + while (stack.isNotEmpty()) { +// Timber.w("VAL: current stack: ${stack.reversed().joinToString { it.name }}") + val vertex = stack.peek() ?: break + // peek a path to follow + var destination: GraphNode? = null + edgesOf(vertex).forEach { + when (visited[it.destination]) { + notVisited -> { + // it's a candidate + destination = it.destination + } + inPath -> { + // Cycle!! + backwardEdges.add(it) + } + completed -> { + // dead end + } + } + } + if (destination == null) { + // dead end, pop + stack.pop().let { + visited[it] = completed + } + } else { + // go down this path + stack.push(destination) + visited[destination!!] = inPath + } + + if (stack.isEmpty()) { + // try to get another graph of forest? + adjacencyList.entries.firstOrNull { visited[it.key] == notVisited }?.key?.let { + stack.push(it) + visited[it] = inPath + } + } + } + + return backwardEdges.toList() + } + + /** + * Only call that on acyclic graph! + */ + fun flattenDestination(): Map<GraphNode, Set<GraphNode>> { + val result = HashMap<GraphNode, Set<GraphNode>>() + adjacencyList.keys.forEach { vertex -> + result[vertex] = flattenOf(vertex) + } + return result + } + + private fun flattenOf(node: GraphNode): Set<GraphNode> { + val result = mutableSetOf<GraphNode>() + val edgesOf = edgesOf(node) + result.addAll(edgesOf.map { it.destination }) + edgesOf.forEach { + result.addAll(flattenOf(it.destination)) + } + return result + } + + override fun toString(): String { + return buildString { + adjacencyList.forEach { (node, edges) -> + append("${node.name} : [") + append(edges.joinToString(" ") { it.destination.name }) + append("]\n") + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..29db8431fd117c12703ef37c2801aee34c24630a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.summary + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +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.util.Optional + +internal class HierarchyLiveDataHelper( + val spaceId: String, + val memberships: List<Membership>, + val roomSummaryDataSource: RoomSummaryDataSource) { + + private val sources = HashMap<String, LiveData<Optional<RoomSummary>>>() + private val mediatorLiveData = MediatorLiveData<List<String>>() + + fun liveData(): LiveData<List<String>> = mediatorLiveData + + init { + onChange() + } + + private fun parentsToCheck(): List<RoomSummary> { + val spaces = ArrayList<RoomSummary>() + roomSummaryDataSource.getSpaceSummary(spaceId)?.let { + roomSummaryDataSource.flattenSubSpace(it, emptyList(), spaces, memberships) + } + return spaces + } + + private fun onChange() { + val existingSources = sources.keys.toList() + val newSources = parentsToCheck().map { it.roomId } + val addedSources = newSources.filter { !existingSources.contains(it) } + val removedSource = existingSources.filter { !newSources.contains(it) } + addedSources.forEach { + val liveData = roomSummaryDataSource.getSpaceSummaryLive(it) + mediatorLiveData.addSource(liveData) { onChange() } + sources[it] = liveData + } + + removedSource.forEach { + sources[it]?.let { mediatorLiveData.removeSource(it) } + } + + sources[spaceId]?.value?.getOrNull()?.let { spaceSummary -> + val results = ArrayList<RoomSummary>() + roomSummaryDataSource.flattenChild(spaceSummary, emptyList(), results, memberships) + mediatorLiveData.postValue(results.map { it.roomId }) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index dd3fbe04b27890a13a3325e0ac39bb28a65c476d..126458b0820e175e874cedf429847bd332bc1228 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -1,5 +1,6 @@ /* * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.summary import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList @@ -24,12 +26,21 @@ import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmQuery import io.realm.Sort +import io.realm.kotlin.where +import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter +import org.matrix.android.sdk.api.session.room.ResultBoundaries +import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams -import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult +import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult +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.RoomType import org.matrix.android.sdk.api.session.room.model.VersioningState +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount +import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper @@ -79,11 +90,60 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> { return monarchy.findAllMappedWithChanges( - { roomSummariesQuery(it, queryParams) }, + { + roomSummariesQuery(it, queryParams) + .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING) + }, { roomSummaryMapper.map(it) } ) } + fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>> { + return getRoomSummariesLive(queryParams) + } + + fun getSpaceSummary(roomIdOrAlias: String): RoomSummary? { + return getRoomSummary(roomIdOrAlias) + ?.takeIf { it.roomType == RoomType.SPACE } + } + + fun getSpaceSummaryLive(roomId: String): LiveData<Optional<RoomSummary>> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> + RoomSummaryEntity.where(realm, roomId) + .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) + .equalTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE) + }, + { + roomSummaryMapper.map(it) + } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<RoomSummary> { + return getRoomSummaries(spaceSummaryQueryParams) + } + + fun getRootSpaceSummaries(): List<RoomSummary> { + return getRoomSummaries(spaceSummaryQueryParams { + memberships = listOf(Membership.JOIN) + }) + .let { allJoinedSpace -> + val allFlattenChildren = arrayListOf<RoomSummary>() + allJoinedSpace.forEach { + flattenSubSpace(it, emptyList(), allFlattenChildren, listOf(Membership.JOIN), false) + } + val knownNonOrphan = allFlattenChildren.map { it.roomId }.distinct() + // keep only root rooms + allJoinedSpace.filter { candidate -> + !knownNonOrphan.contains(candidate.roomId) + } + } + } + fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> { return monarchy.fetchAllMappedSync( { breadcrumbsQuery(it, queryParams) }, @@ -105,10 +165,10 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat } fun getSortedPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, - pagedListConfig: PagedList.Config): LiveData<PagedList<RoomSummary>> { + pagedListConfig: PagedList.Config, + sortOrder: RoomSortOrder): LiveData<PagedList<RoomSummary>> { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - roomSummariesQuery(realm, queryParams) - .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING) + roomSummariesQuery(realm, queryParams).process(sortOrder) } val dataSourceFactory = realmDataSourceFactory.map { roomSummaryMapper.map(it) @@ -119,30 +179,48 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat ) } - fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, - pagedListConfig: PagedList.Config): UpdatableFilterLivePageResult { + fun getUpdatablePagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, + pagedListConfig: PagedList.Config, + sortOrder: RoomSortOrder): UpdatableLivePageResult { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - roomSummariesQuery(realm, queryParams) - .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING) + roomSummariesQuery(realm, queryParams).process(sortOrder) } val dataSourceFactory = realmDataSourceFactory.map { roomSummaryMapper.map(it) } + val boundaries = MutableLiveData(ResultBoundaries()) + val mapped = monarchy.findAllPagedWithChanges( realmDataSourceFactory, - LivePagedListBuilder(dataSourceFactory, pagedListConfig) + LivePagedListBuilder(dataSourceFactory, pagedListConfig).also { + it.setBoundaryCallback(object : PagedList.BoundaryCallback<RoomSummary>() { + override fun onItemAtEndLoaded(itemAtEnd: RoomSummary) { + boundaries.postValue(boundaries.value?.copy(frontLoaded = true)) + } + + override fun onItemAtFrontLoaded(itemAtFront: RoomSummary) { + boundaries.postValue(boundaries.value?.copy(endLoaded = true)) + } + + override fun onZeroItemsLoaded() { + boundaries.postValue(boundaries.value?.copy(zeroItemLoaded = true)) + } + }) + } ) - return object : UpdatableFilterLivePageResult { + return object : UpdatableLivePageResult { override val livePagedList: LiveData<PagedList<RoomSummary>> = mapped - override fun updateQuery(queryParams: RoomSummaryQueryParams) { + override fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) { realmDataSourceFactory.updateQuery { - roomSummariesQuery(it, queryParams) - .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING) + roomSummariesQuery(it, builder.invoke(queryParams)).process(sortOrder) } } + + override val liveBoundaries: LiveData<ResultBoundaries> + get() = boundaries } } @@ -170,10 +248,10 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat queryParams.roomCategoryFilter?.let { when (it) { - RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) - RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0) - RoomCategoryFilter.ALL -> { + RoomCategoryFilter.ALL -> { // nop } } @@ -189,6 +267,160 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat query.equalTo(RoomSummaryEntityFields.IS_SERVER_NOTICE, sn) } } + + queryParams.excludeType?.forEach { + query.notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, it) + } + queryParams.includeType?.forEach { + query.equalTo(RoomSummaryEntityFields.ROOM_TYPE, it) + } + when (queryParams.roomCategoryFilter) { + RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0) + RoomCategoryFilter.ALL -> Unit // nop + } + + // Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}") + when (queryParams.activeSpaceFilter) { + is ActiveSpaceFilter.ActiveSpace -> { + // It's annoying but for now realm java does not support querying in primitive list :/ + // https://github.com/realm/realm-java/issues/5361 + if (queryParams.activeSpaceFilter.currentSpaceId == null) { + // orphan rooms + query.isNull(RoomSummaryEntityFields.FLATTEN_PARENT_IDS) + } else { + query.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceFilter.currentSpaceId) + } + } + is ActiveSpaceFilter.ExcludeSpace -> { + query.not().contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceFilter.spaceId) + } + else -> { + // nop + } + } + + if (queryParams.activeGroupId != null) { + query.contains(RoomSummaryEntityFields.GROUP_IDS, queryParams.activeGroupId!!) + } return query } + + fun getAllRoomSummaryChildOf(spaceAliasOrId: String, memberShips: List<Membership>): List<RoomSummary> { + val space = getSpaceSummary(spaceAliasOrId) ?: return emptyList() + val result = ArrayList<RoomSummary>() + flattenChild(space, emptyList(), result, memberShips) + return result + } + + fun getAllRoomSummaryChildOfLive(spaceId: String, memberShips: List<Membership>): LiveData<List<RoomSummary>> { + // we want to listen to all spaces in hierarchy and on change compute back all childs + // and switch map to listen those? + val mediatorLiveData = HierarchyLiveDataHelper(spaceId, memberShips, this).liveData() + + return Transformations.switchMap(mediatorLiveData) { allIds -> + monarchy.findAllMappedWithChanges( + { + it.where<RoomSummaryEntity>() + .`in`(RoomSummaryEntityFields.ROOM_ID, allIds.toTypedArray()) + .`in`(RoomSummaryEntityFields.MEMBERSHIP_STR, memberShips.map { it.name }.toTypedArray()) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + }, + { + roomSummaryMapper.map(it) + }) + } + } + + fun getFlattenOrphanRooms(): List<RoomSummary> { + return getRoomSummaries( + roomSummaryQueryParams { + memberships = Membership.activeMemberships() + excludeType = listOf(RoomType.SPACE) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + } + ).filter { isOrphan(it) } + } + + fun getFlattenOrphanRoomsLive(): LiveData<List<RoomSummary>> { + return Transformations.map( + getRoomSummariesLive(roomSummaryQueryParams { + memberships = Membership.activeMemberships() + excludeType = listOf(RoomType.SPACE) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + }) + ) { + it.filter { isOrphan(it) } + } + } + + private fun isOrphan(roomSummary: RoomSummary): Boolean { + if (roomSummary.roomType == RoomType.SPACE && roomSummary.membership.isActive()) { + return false + } + // all parents line should be orphan + roomSummary.spaceParents?.forEach { info -> + if (info.roomSummary != null && !info.roomSummary.membership.isLeft()) { + if (!isOrphan(info.roomSummary)) { + return false + } + } + } + + // it may not have a parent relation but could be a child of some other.... + for (spaceSummary in getSpaceSummaries(spaceSummaryQueryParams { memberships = Membership.activeMemberships() })) { + if (spaceSummary.spaceChildren?.any { it.childRoomId == roomSummary.roomId } == true) { + return false + } + } + + return true + } + + fun flattenChild(current: RoomSummary, parenting: List<String>, output: MutableList<RoomSummary>, memberShips: List<Membership>) { + current.spaceChildren?.sortedBy { it.order ?: it.name }?.forEach { childInfo -> + if (childInfo.roomType == RoomType.SPACE) { + // Add recursive + if (!parenting.contains(childInfo.childRoomId)) { // avoid cycles! + getSpaceSummary(childInfo.childRoomId)?.let { subSpace -> + if (memberShips.isEmpty() || memberShips.contains(subSpace.membership)) { + flattenChild(subSpace, parenting + listOf(current.roomId), output, memberShips) + } + } + } + } else if (childInfo.isKnown) { + getRoomSummary(childInfo.childRoomId)?.let { + if (memberShips.isEmpty() || memberShips.contains(it.membership)) { + if (!it.isDirect) { + output.add(it) + } + } + } + } + } + } + + fun flattenSubSpace(current: RoomSummary, + parenting: List<String>, + output: MutableList<RoomSummary>, + memberShips: List<Membership>, + includeCurrent: Boolean = true) { + if (includeCurrent) { + output.add(current) + } + current.spaceChildren?.sortedBy { it.order ?: it.name }?.forEach { + if (it.roomType == RoomType.SPACE) { + // Add recursive + if (!parenting.contains(it.childRoomId)) { // avoid cycles! + getSpaceSummary(it.childRoomId)?.let { subSpace -> + if (memberShips.isEmpty() || memberShips.contains(subSpace.membership)) { + output.add(subSpace) + flattenSubSpace(subSpace, parenting + listOf(current.roomId), output, memberShips) + } + } + } + } + } + } } 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 7913bf71a2ac69f55687fc0c840e8091ac47af56..d488fdfc2a91fa07e400afd7a170715a355a5b1d 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 @@ -17,14 +17,18 @@ package org.matrix.android.sdk.internal.session.room.summary import io.realm.Realm +import io.realm.kotlin.createObject 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 import org.matrix.android.sdk.api.session.room.model.RoomAliasesContent import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent +import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent 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.model.RoomType +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent 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 @@ -34,29 +38,40 @@ 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 +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity 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.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntity +import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.isEventRead +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.extensions.clearWith +import org.matrix.android.sdk.internal.query.process 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.relationship.RoomChildRelationInfo +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications import timber.log.Timber import javax.inject.Inject +import kotlin.system.measureTimeMillis internal class RoomSummaryUpdater @Inject constructor( @UserId private val userId: String, private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomAvatarResolver: RoomAvatarResolver, private val eventDecryptor: EventDecryptor, - private val crossSigningService: DefaultCrossSigningService) { + private val crossSigningService: DefaultCrossSigningService, + private val stateEventDataSource: StateEventDataSource) { fun update(realm: Realm, roomId: String, @@ -89,6 +104,12 @@ internal class RoomSummaryUpdater @Inject constructor( val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root + val roomCreateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CREATE, stateKey = "")?.root + val joinRulesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_JOIN_RULES, stateKey = "")?.root + + val roomType = ContentMapper.map(roomCreateEvent?.content).toModel<RoomCreateContent>()?.type + roomSummaryEntity.roomType = roomType + Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]") // Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) @@ -111,6 +132,7 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) roomSummaryEntity.name = ContentMapper.map(lastNameEvent?.content).toModel<RoomNameContent>()?.name roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic + roomSummaryEntity.joinRules = ContentMapper.map(joinRulesEvent?.content).toModel<RoomJoinRulesContent>()?.joinRules roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>() ?.canonicalAlias @@ -163,4 +185,234 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.updateHasFailedSending() roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) } + + /** + * Should be called at the end of the room sync, to check and validate all parent/child relations + */ + fun validateSpaceRelationship(realm: Realm) { + measureTimeMillis { + val lookupMap = realm.where(RoomSummaryEntity::class.java) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + // we order by roomID to be consistent when breaking parent/child cycles + .sort(RoomSummaryEntityFields.ROOM_ID) + .findAll().map { + it.flattenParentIds = null + it to emptyList<RoomSummaryEntity>().toMutableSet() + } + .toMap() + + lookupMap.keys.forEach { lookedUp -> + if (lookedUp.roomType == RoomType.SPACE) { + // get childrens + + lookedUp.children.clearWith { it.deleteFromRealm() } + + RoomChildRelationInfo(realm, lookedUp.roomId).getDirectChildrenDescriptions().forEach { child -> + + lookedUp.children.add( + realm.createObject<SpaceChildSummaryEntity>().apply { + this.childRoomId = child.roomId + this.childSummaryEntity = RoomSummaryEntity.where(realm, child.roomId).findFirst() + this.order = child.order + this.autoJoin = child.autoJoin + this.viaServers.addAll(child.viaServers) + } + ) + + RoomSummaryEntity.where(realm, child.roomId) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) + .findFirst() + ?.let { childSum -> + lookupMap.entries.firstOrNull { it.key.roomId == lookedUp.roomId }?.let { entry -> + if (entry.value.indexOfFirst { it.roomId == childSum.roomId } == -1) { + // add looked up as a parent + entry.value.add(childSum) + } + } + } + } + } else { + lookedUp.parents.clearWith { it.deleteFromRealm() } + // can we check parent relations here?? + RoomChildRelationInfo(realm, lookedUp.roomId).getParentDescriptions() + .map { parentInfo -> + + lookedUp.parents.add( + realm.createObject<SpaceParentSummaryEntity>().apply { + this.parentRoomId = parentInfo.roomId + this.parentSummaryEntity = RoomSummaryEntity.where(realm, parentInfo.roomId).findFirst() + this.canonical = parentInfo.canonical + this.viaServers.addAll(parentInfo.viaServers) + } + ) + + RoomSummaryEntity.where(realm, parentInfo.roomId) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) + .findFirst() + ?.let { parentSum -> + if (lookupMap[parentSum]?.indexOfFirst { it.roomId == lookedUp.roomId } == -1) { + // add lookedup as a parent + lookupMap[parentSum]?.add(lookedUp) + } + } + } + } + } + + // Simple algorithm to break cycles + // Need more work to decide how to break, probably need to be as consistent as possible + // and also find best way to root the tree + + val graph = Graph() + lookupMap + // focus only on joined spaces, as room are just leaf + .filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN } + .forEach { (sum, children) -> + graph.getOrCreateNode(sum.roomId) + children.forEach { + graph.addEdge(it.roomId, sum.roomId) + } + } + + val backEdges = graph.findBackwardEdges() + Timber.v("## SPACES: Cycle detected = ${backEdges.isNotEmpty()}") + + // break cycles + backEdges.forEach { edge -> + lookupMap.entries.find { it.key.roomId == edge.source.name }?.let { + it.value.removeAll { it.roomId == edge.destination.name } + } + } + + val acyclicGraph = graph.withoutEdges(backEdges) +// Timber.v("## SPACES: acyclicGraph $acyclicGraph") + val flattenSpaceParents = acyclicGraph.flattenDestination().map { + it.key.name to it.value.map { it.name } + }.toMap() +// Timber.v("## SPACES: flattenSpaceParents ${flattenSpaceParents.map { it.key.name to it.value.map { it.name } }.joinToString("\n") { +// it.first + ": [" + it.second.joinToString(",") + "]" +// }}") + +// Timber.v("## SPACES: lookup map ${lookupMap.map { it.key.name to it.value.map { it.name } }.toMap()}") + + lookupMap.entries + .filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN } + .forEach { entry -> + val parent = RoomSummaryEntity.where(realm, entry.key.roomId).findFirst() + if (parent != null) { +// Timber.v("## SPACES: check hierarchy of ${parent.name} id ${parent.roomId}") +// Timber.v("## SPACES: flat known parents of ${parent.name} are ${flattenSpaceParents[parent.roomId]}") + val flattenParentsIds = (flattenSpaceParents[parent.roomId] ?: emptyList()) + listOf(parent.roomId) +// Timber.v("## SPACES: flatten known parents of children of ${parent.name} are ${flattenParentsIds}") + entry.value.forEach { child -> + RoomSummaryEntity.where(realm, child.roomId).findFirst()?.let { childSum -> + +// Timber.w("## SPACES: ${childSum.name} is ${childSum.roomId} fc: ${childSum.flattenParentIds}") +// var allParents = childSum.flattenParentIds ?: "" + if (childSum.flattenParentIds == null) childSum.flattenParentIds = "" + flattenParentsIds.forEach { + if (childSum.flattenParentIds?.contains(it) != true) { + childSum.flattenParentIds += "|$it" + } + } +// childSum.flattenParentIds = "$allParents|" + +// Timber.v("## SPACES: flatten of ${childSum.name} is ${childSum.flattenParentIds}") + } + } + } + } + + // we need also to filter DMs... + // it's more annoying as based on if the other members belong the space or not + RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) + .findAll() + .forEach { dmRoom -> + val relatedSpaces = lookupMap.keys + .filter { it.roomType == RoomType.SPACE } + .filter { + dmRoom.otherMemberIds.toList().intersect(it.otherMemberIds.toList()).isNotEmpty() + } + .map { it.roomId } + .distinct() + val flattenRelated = mutableListOf<String>().apply { + addAll(relatedSpaces) + relatedSpaces.map { flattenSpaceParents[it] }.forEach { + if (it != null) addAll(it) + } + }.distinct() + if (flattenRelated.isNotEmpty()) { + // we keep real m.child/m.parent relations and add the one for common memberships + dmRoom.flattenParentIds += "|${flattenRelated.joinToString("|")}|" + } +// Timber.v("## SPACES: flatten of ${dmRoom.otherMemberIds.joinToString(",")} is ${dmRoom.flattenParentIds}") + } + + // Maybe a good place to count the number of notifications for spaces? + + realm.where(RoomSummaryEntity::class.java) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) + .equalTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE) + .findAll().forEach { space -> + // get all children + var highlightCount = 0 + var notificationCount = 0 + realm.where(RoomSummaryEntity::class.java) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, listOf(Membership.JOIN)) + .notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE) + // also we do not count DM in here, because home space will already show them + .equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + .contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, space.roomId) + .findAll().forEach { + highlightCount += it.highlightCount + notificationCount += it.notificationCount + } + + space.highlightCount = highlightCount + space.notificationCount = notificationCount + } + // xxx invites?? + + // LEGACY GROUPS + // lets mark rooms that belongs to groups + val existingGroups = GroupSummaryEntity.where(realm).findAll() + + // For rooms + realm.where(RoomSummaryEntity::class.java) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + .findAll().forEach { room -> + val belongsTo = existingGroups.filter { it.roomIds.contains(room.roomId) } + room.groupIds = if (belongsTo.isEmpty()) { + null + } else { + "|${belongsTo.joinToString("|")}|" + } + } + + // For DMS + realm.where(RoomSummaryEntity::class.java) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll().forEach { room -> + val belongsTo = existingGroups.filter { + it.userIds.intersect(room.otherMemberIds).isNotEmpty() + } + room.groupIds = if (belongsTo.isEmpty()) { + null + } else { + "|${belongsTo.joinToString("|")}|" + } + } + }.also { + Timber.v("## SPACES: Finish checking room hierarchy in $it ms") + } + } + +// private fun isValidCanonical() : Boolean { +// +// } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index c38dcd00a73387e3ec1fe0697a607ef2bd8141ed..a7cba2fe99ce93432a31e48638b8630080692291 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -149,7 +149,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri } ?: ChunkEntity.create(realm, prevToken, nextToken) - if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) { + if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) { handleReachEnd(realm, roomId, direction, currentChunk) } else { handlePagination(realm, roomId, direction, receivedChunk, currentChunk) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt index c3b2d7f161998bc38abccec3832cca4967cec2ce..fad1840e51b07349427572f59e3744a321f63dc0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt @@ -18,12 +18,14 @@ package org.matrix.android.sdk.internal.session.securestorage +import android.annotation.SuppressLint import android.content.Context import android.os.Build import android.security.KeyPairGeneratorSpec import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import androidx.annotation.RequiresApi +import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider import timber.log.Timber import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -32,6 +34,7 @@ import java.io.InputStream import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.OutputStream +import java.lang.IllegalArgumentException import java.math.BigInteger import java.security.KeyPairGenerator import java.security.KeyStore @@ -58,23 +61,19 @@ import javax.security.auth.x500.X500Principal * is not available. * * <b>Android [K-M[</b> - * For android >=KITKAT and <M, we use the keystore to generate and store a private/public key pair. Then for each secret, a + * For android >=L and <M, we use the keystore to generate and store a private/public key pair. Then for each secret, a * random secret key in generated to perform encryption. * This secret key is encrypted with the public RSA key and stored with the encrypted secret. * In order to decrypt the encrypted secret key will be retrieved then decrypted with the RSA private key. * - * <b>Older androids</b> - * For older androids as a fallback we generate an AES key from the alias using PBKDF2 with random salt. - * The salt and iv are stored with encrypted data. - * * Sample usage: * <code> * val secret = "The answer is 42" - * val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias", context) + * val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias") * //This can be stored anywhere e.g. encoded in b64 and stored in preference for example * * //to get back the secret, just call - * val kDecrypted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context) + * val kDecrypted = SecretStoringUtils.loadSecureSecret(KEncrypted, "myAlias") * </code> * * You can also just use this utility to store a secret key, and use any encryption algorithm that you want. @@ -82,7 +81,10 @@ import javax.security.auth.x500.X500Principal * Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you * add a pin or change the schema); So you might and with a useless pile of bytes. */ -internal class SecretStoringUtils @Inject constructor(private val context: Context) { +internal class SecretStoringUtils @Inject constructor( + private val context: Context, + private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider +) { companion object { private const val ANDROID_KEY_STORE = "AndroidKeyStore" @@ -91,7 +93,6 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte private const val FORMAT_API_M: Byte = 0 private const val FORMAT_1: Byte = 1 - private const val FORMAT_2: Byte = 2 } private val keyStore: KeyStore by lazy { @@ -113,43 +114,52 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte /** * Encrypt the given secret using the android Keystore. * On android >= M, will directly use the keystore to generate a symmetric key - * On android >= KitKat and <M, as symmetric key gen is not available, will use an symmetric key generated + * On android >= Lollipop and <M, as symmetric key gen is not available, will use an symmetric key generated * in the keystore to encrypted a random symmetric key. The encrypted symmetric key is returned * in the bytearray (in can be stored anywhere, it is encrypted) - * On older version a key in generated from alias with random salt. * * The secret is encrypted using the following method: AES/GCM/NoPadding */ + @SuppressLint("NewApi") @Throws(Exception::class) - fun securelyStoreString(secret: String, keyAlias: String): ByteArray? { + fun securelyStoreString(secret: String, keyAlias: String): ByteArray { return when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias) - else -> encryptString(secret, keyAlias) + buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias) + else -> encryptString(secret, keyAlias) } } /** * Decrypt a secret that was encrypted by #securelyStoreString() */ + @SuppressLint("NewApi") @Throws(Exception::class) - fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String? { - return when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias) - else -> decryptString(encrypted, keyAlias) + fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String { + encrypted.inputStream().use { inputStream -> + // First get the format + return when (val format = inputStream.read().toByte()) { + FORMAT_API_M -> decryptStringM(inputStream, keyAlias) + FORMAT_1 -> decryptString(inputStream, keyAlias) + else -> throw IllegalArgumentException("Unknown format $format") + } } } + @SuppressLint("NewApi") fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) { when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any) - else -> saveSecureObject(keyAlias, output, any) + buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any) + else -> saveSecureObject(keyAlias, output, any) } } + @SuppressLint("NewApi") fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { - return when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream) - else -> loadSecureObject(keyAlias, inputStream) + // First get the format + return when (val format = inputStream.read().toByte()) { + FORMAT_API_M -> loadSecureObjectM(keyAlias, inputStream) + FORMAT_1 -> loadSecureObject(keyAlias, inputStream) + else -> throw IllegalArgumentException("Unknown format $format") } } @@ -180,7 +190,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte - Store the encrypted AES Generate a key pair for encryption */ - fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry { + private fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry { val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry) if (privateKeyEntry != null) return privateKeyEntry @@ -193,7 +203,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte .setAlias(alias) .setSubject(X500Principal("CN=$alias")) .setSerialNumber(BigInteger.TEN) - // .setEncryptionRequired() requires that the phone as a pin/schema + // .setEncryptionRequired() requires that the phone has a pin/schema .setStartDate(start.time) .setEndDate(end.time) .build() @@ -205,7 +215,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte } @RequiresApi(Build.VERSION_CODES.M) - fun encryptStringM(text: String, keyAlias: String): ByteArray? { + private fun encryptStringM(text: String, keyAlias: String): ByteArray { val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) val cipher = Cipher.getInstance(AES_MODE) @@ -217,8 +227,8 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte } @RequiresApi(Build.VERSION_CODES.M) - private fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String { - val (iv, encryptedText) = formatMExtract(encryptedChunk.inputStream()) + private fun decryptStringM(inputStream: InputStream, keyAlias: String): String { + val (iv, encryptedText) = formatMExtract(inputStream) val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) @@ -229,7 +239,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte return String(cipher.doFinal(encryptedText), Charsets.UTF_8) } - private fun encryptString(text: String, keyAlias: String): ByteArray? { + private fun encryptString(text: String, keyAlias: String): ByteArray { // we generate a random symmetric key val key = ByteArray(16) secureRandom.nextBytes(key) @@ -246,8 +256,8 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte return format1Make(encryptedKey, iv, encryptedBytes) } - private fun decryptString(data: ByteArray, keyAlias: String): String? { - val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data)) + private fun decryptString(inputStream: InputStream, keyAlias: String): String { + val (encryptedKey, iv, encrypted) = format1Extract(inputStream) // we need to decrypt the key val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey)) @@ -307,30 +317,11 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte output.write(bos1.toByteArray()) } -// @RequiresApi(Build.VERSION_CODES.M) -// @Throws(IOException::class) -// fun saveSecureObjectM(keyAlias: String, file: File, writeObject: Any) { -// FileOutputStream(file).use { -// saveSecureObjectM(keyAlias, it, writeObject) -// } -// } -// -// @RequiresApi(Build.VERSION_CODES.M) -// @Throws(IOException::class) -// fun <T> loadSecureObjectM(keyAlias: String, file: File): T? { -// FileInputStream(file).use { -// return loadSecureObjectM<T>(keyAlias, it) -// } -// } - @RequiresApi(Build.VERSION_CODES.M) @Throws(IOException::class) private fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? { val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) - val format = inputStream.read() - assert(format.toByte() == FORMAT_API_M) - val ivSize = inputStream.read() val iv = ByteArray(ivSize) inputStream.read(iv, 0, ivSize) @@ -393,9 +384,6 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte } private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> { - val format = bis.read().toByte() - assert(format == FORMAT_API_M) - val ivSize = bis.read() val iv = ByteArray(ivSize) bis.read(iv, 0, ivSize) @@ -414,9 +402,6 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte } private fun format1Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> { - val format = bis.read() - assert(format.toByte() == FORMAT_1) - val keySizeBig = bis.read() val keySizeLow = bis.read() val encryptedKeySize = keySizeBig.shl(8) + keySizeLow @@ -443,32 +428,4 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte return bos.toByteArray() } - - private fun format2Make(salt: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray { - val bos = ByteArrayOutputStream(3 + salt.size + iv.size + encryptedBytes.size) - bos.write(FORMAT_2.toInt()) - bos.write(salt.size) - bos.write(salt) - bos.write(iv.size) - bos.write(iv) - bos.write(encryptedBytes) - - return bos.toByteArray() - } - - private fun format2Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> { - val format = bis.read() - assert(format.toByte() == FORMAT_2) - - val saltSize = bis.read() - val salt = ByteArray(saltSize) - bis.read(salt) - - val ivSize = bis.read() - val iv = ByteArray(ivSize) - bis.read(iv) - - val encrypted = bis.readBytes() - return Triple(salt, iv, encrypted) - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt new file mode 100644 index 0000000000000000000000000000000000000000..70c52bf4aec8169596a706a6929af844e774b65c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.space.Space +import org.matrix.android.sdk.api.session.space.model.SpaceChildContent +import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource + +internal class DefaultSpace( + private val room: Room, + private val spaceSummaryDataSource: RoomSummaryDataSource, + private val viaParameterFinder: ViaParameterFinder +) : Space { + + override fun asRoom(): Room { + return room + } + + override val spaceId = room.roomId + + override suspend fun leave(reason: String?) { + return room.leave(reason) + } + + override fun spaceSummary(): RoomSummary? { + return spaceSummaryDataSource.getSpaceSummary(room.roomId) + } + + override suspend fun addChildren(roomId: String, + viaServers: List<String>?, + order: String?, + autoJoin: Boolean, + suggested: Boolean?) { + // Find best via + + room.sendStateEvent( + eventType = EventType.STATE_SPACE_CHILD, + stateKey = roomId, + body = SpaceChildContent( + via = viaServers ?: viaParameterFinder.computeViaParams(roomId, 3), + autoJoin = autoJoin, + order = order, + suggested = suggested + ).toContent() + ) + } + + override suspend fun removeChildren(roomId: String) { +// val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) +// .firstOrNull() +// ?.content.toModel<SpaceChildContent>() +// ?: // should we throw here? +// return + + // edit state event and set via to null + room.sendStateEvent( + eventType = EventType.STATE_SPACE_CHILD, + stateKey = roomId, + body = SpaceChildContent( + order = null, + via = null, + autoJoin = null, + suggested = null + ).toContent() + ) + } + + override suspend fun setChildrenOrder(roomId: String, order: String?) { + val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) + .firstOrNull() + ?.content.toModel<SpaceChildContent>() + ?: throw IllegalArgumentException("$roomId is not a child of this space") + + // edit state event and set via to null + room.sendStateEvent( + eventType = EventType.STATE_SPACE_CHILD, + stateKey = roomId, + body = SpaceChildContent( + order = order, + via = existing.via, + autoJoin = existing.autoJoin, + suggested = existing.suggested + ).toContent() + ) + } + + override suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) { + val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) + .firstOrNull() + ?.content.toModel<SpaceChildContent>() + ?: throw IllegalArgumentException("$roomId is not a child of this space") + + if (existing.autoJoin == autoJoin) { + // nothing to do? + return + } + + // edit state event and set via to null + room.sendStateEvent( + eventType = EventType.STATE_SPACE_CHILD, + stateKey = roomId, + body = SpaceChildContent( + order = existing.order, + via = existing.via, + autoJoin = autoJoin, + suggested = existing.suggested + ).toContent() + ) + } + + override suspend fun setChildrenSuggested(roomId: String, suggested: Boolean) { + val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) + .firstOrNull() + ?.content.toModel<SpaceChildContent>() + ?: throw IllegalArgumentException("$roomId is not a child of this space") + + if (existing.suggested == suggested) { + // nothing to do? + return + } + // edit state event and set via to null + room.sendStateEvent( + eventType = EventType.STATE_SPACE_CHILD, + stateKey = roomId, + body = SpaceChildContent( + order = existing.order, + via = existing.via, + autoJoin = existing.autoJoin, + suggested = suggested + ).toContent() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt new file mode 100644 index 0000000000000000000000000000000000000000..d0ad19245f9860a2982b96a1eac15d865bd12637 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -0,0 +1,207 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import android.net.Uri +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.space.CreateSpaceParams +import org.matrix.android.sdk.api.session.space.JoinSpaceResult +import org.matrix.android.sdk.api.session.space.Space +import org.matrix.android.sdk.api.session.space.SpaceService +import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams +import org.matrix.android.sdk.api.session.space.model.SpaceChildContent +import org.matrix.android.sdk.api.session.space.model.SpaceParentContent +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.RoomGetter +import org.matrix.android.sdk.internal.session.room.SpaceGetter +import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask +import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource +import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask +import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult +import javax.inject.Inject + +internal class DefaultSpaceService @Inject constructor( + @UserId private val userId: String, + private val createRoomTask: CreateRoomTask, + private val joinSpaceTask: JoinSpaceTask, + private val spaceGetter: SpaceGetter, + private val roomGetter: RoomGetter, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val stateEventDataSource: StateEventDataSource, + private val peekSpaceTask: PeekSpaceTask, + private val resolveSpaceInfoTask: ResolveSpaceInfoTask, + private val leaveRoomTask: LeaveRoomTask +) : SpaceService { + + override suspend fun createSpace(params: CreateSpaceParams): String { + return createRoomTask.executeRetry(params, 3) + } + + override suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String { + return createSpace(CreateSpaceParams().apply { + this.name = name + this.topic = topic + this.avatarUri = avatarUri + if (isPublic) { + this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy( + invite = 0 + ) + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE + this.guestAccess = GuestAccess.CanJoin + } else { + this.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT + visibility = RoomDirectoryVisibility.PRIVATE + } + }) + } + + override fun getSpace(spaceId: String): Space? { + return spaceGetter.get(spaceId) + } + + override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>> { + return roomSummaryDataSource.getSpaceSummariesLive(queryParams) + } + + override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<RoomSummary> { + return roomSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams) + } + + override fun getRootSpaceSummaries(): List<RoomSummary> { + return roomSummaryDataSource.getRootSpaceSummaries() + } + + override suspend fun peekSpace(spaceId: String): SpacePeekResult { + return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId)) + } + + override suspend fun querySpaceChildren(spaceId: String, + suggestedOnly: Boolean?, + autoJoinedOnly: Boolean?): Pair<RoomSummary, List<SpaceChildInfo>> { + return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId, suggestedOnly, autoJoinedOnly)).let { response -> + val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId } + Pair( + first = RoomSummary( + roomId = spaceDesc?.roomId ?: spaceId, + roomType = spaceDesc?.roomType, + name = spaceDesc?.name ?: "", + displayName = spaceDesc?.name ?: "", + topic = spaceDesc?.topic ?: "", + joinedMembersCount = spaceDesc?.numJoinedMembers, + avatarUrl = spaceDesc?.avatarUrl ?: "", + encryptionEventTs = null, + typingUsers = emptyList(), + isEncrypted = false, + flattenParentIds = emptyList() + ), + second = response.rooms + ?.filter { it.roomId != spaceId } + ?.flatMap { childSummary -> + response.events + ?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD } + ?.mapNotNull { childStateEv -> + // create a child entry for everytime this room is the child of a space + // beware that a room could appear then twice in this list + childStateEv.content.toModel<SpaceChildContent>()?.let { childStateEvContent -> + SpaceChildInfo( + childRoomId = childSummary.roomId, + isKnown = true, + roomType = childSummary.roomType, + name = childSummary.name, + topic = childSummary.topic, + avatarUrl = childSummary.avatarUrl, + order = childStateEvContent.order, + autoJoin = childStateEvContent.autoJoin ?: false, + viaServers = childStateEvContent.via.orEmpty(), + activeMemberCount = childSummary.numJoinedMembers, + parentRoomId = childStateEv.roomId, + suggested = childStateEvContent.suggested, + canonicalAlias = childSummary.canonicalAlias, + aliases = childSummary.aliases + ) + } + }.orEmpty() + } + .orEmpty() + ) + } + } + + override suspend fun joinSpace(spaceIdOrAlias: String, + reason: String?, + viaServers: List<String>): JoinSpaceResult { + return joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers)) + } + + override suspend fun rejectInvite(spaceId: String, reason: String?) { + leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason)) + } + +// override fun getSpaceParentsOfRoom(roomId: String): List<SpaceSummary> { +// return spaceSummaryDataSource.getParentsOfRoom(roomId) +// } + + override suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List<String>) { + // Should we perform some validation here?, + // and if client want to bypass, it could use sendStateEvent directly? + if (canonical) { + // check that we can send m.child in the parent room + if (roomSummaryDataSource.getRoomSummary(parentSpaceId)?.membership != Membership.JOIN) { + throw UnsupportedOperationException("Cannot add canonical child if not member of parent") + } + val powerLevelsEvent = stateEventDataSource.getStateEvent( + roomId = parentSpaceId, + eventType = EventType.STATE_ROOM_POWER_LEVELS, + stateKey = QueryStringValue.NoCondition + ) + val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() + ?: throw UnsupportedOperationException("Cannot add canonical child, missing powerlevel") + val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) + if (!powerLevelsHelper.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) { + throw UnsupportedOperationException("Cannot add canonical child, not enough power level") + } + } + + val room = roomGetter.getRoom(childRoomId) + ?: throw IllegalArgumentException("Unknown Room $childRoomId") + + room.sendStateEvent( + eventType = EventType.STATE_SPACE_PARENT, + stateKey = parentSpaceId, + body = SpaceParentContent( + via = viaServers, + canonical = canonical + ).toContent() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..e9d5ba519370037fc306c58448eed3f45fe5b750 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.space.JoinSpaceResult +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal interface JoinSpaceTask : Task<JoinSpaceTask.Params, JoinSpaceResult> { + data class Params( + val roomIdOrAlias: String, + val reason: String?, + val viaServers: List<String> = emptyList() + ) +} + +internal class DefaultJoinSpaceTask @Inject constructor( + private val joinRoomTask: JoinRoomTask, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, + private val roomSummaryDataSource: RoomSummaryDataSource +) : JoinSpaceTask { + + override suspend fun execute(params: JoinSpaceTask.Params): JoinSpaceResult { + Timber.v("## Space: > Joining root space ${params.roomIdOrAlias} ...") + try { + joinRoomTask.execute(JoinRoomTask.Params( + params.roomIdOrAlias, + params.reason, + params.viaServers + )) + } catch (failure: Throwable) { + return JoinSpaceResult.Fail(failure) + } + Timber.v("## Space: < Joining root space done for ${params.roomIdOrAlias}") + // we want to wait for sync result to check for auto join rooms + + Timber.v("## Space: > Wait for post joined sync ${params.roomIdOrAlias} ...") + try { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(2L)) { realm -> + realm.where(RoomSummaryEntity::class.java) + .apply { + if (params.roomIdOrAlias.startsWith("!")) { + equalTo(RoomSummaryEntityFields.ROOM_ID, params.roomIdOrAlias) + } else { + equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, params.roomIdOrAlias) + } + } + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + } + } catch (exception: TimeoutCancellationException) { + Timber.w("## Space: > Error created with timeout") + return JoinSpaceResult.PartialSuccess(emptyMap()) + } + + val errors = mutableMapOf<String, Throwable>() + Timber.v("## Space: > Sync done ...") + // after that i should have the children (? do I need to paginate to get state) + val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias) + Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.spaceChildren?.size}") + summary?.spaceChildren?.forEach { +// val childRoomSummary = it.roomSummary ?: return@forEach + Timber.v("## Space: Processing child :[${it.childRoomId}] autoJoin:${it.autoJoin}") + if (it.autoJoin) { + // I should try to join as well + if (it.roomType == RoomType.SPACE) { + // recursively join auto-joined child of this space? + when (val subspaceJoinResult = execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) { + JoinSpaceResult.Success -> { + // nop + } + is JoinSpaceResult.Fail -> { + errors[it.childRoomId] = subspaceJoinResult.error + } + is JoinSpaceResult.PartialSuccess -> { + errors.putAll(subspaceJoinResult.failedRooms) + } + } + } else { + try { + Timber.v("## Space: Joining room child ${it.childRoomId}") + joinRoomTask.execute(JoinRoomTask.Params( + roomIdOrAlias = it.childRoomId, + reason = "Auto-join parent space", + viaServers = it.viaServers + )) + } catch (failure: Throwable) { + errors[it.childRoomId] = failure + Timber.e("## Space: Failed to join room child ${it.childRoomId}") + } + } + } + } + + return if (errors.isEmpty()) { + JoinSpaceResult.Success + } else { + JoinSpaceResult.PartialSuccess(errors) + } + } +} + +// try { +// awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> +// realm.where(RoomEntity::class.java) +// .equalTo(RoomEntityFields.ROOM_ID, roomId) +// } +// } catch (exception: TimeoutCancellationException) { +// throw CreateRoomFailure.CreatedWithTimeout +// } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..d2be49b70b0f0f603f2fa79f2f58578ea00d85f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, SpacesResponse> { + data class Params( + val spaceId: String, + val maxRoomPerSpace: Int?, + val limit: Int, + val batchToken: String?, + val suggestedOnly: Boolean?, + val autoJoinOnly: Boolean? + ) { + companion object { + fun withId(spaceId: String, suggestedOnly: Boolean?, autoJoinOnly: Boolean?) = + Params( + spaceId = spaceId, + maxRoomPerSpace = 10, + limit = 20, + batchToken = null, + suggestedOnly = suggestedOnly, + autoJoinOnly = autoJoinOnly + ) + } + } +} + +internal class DefaultResolveSpaceInfoTask @Inject constructor( + private val spaceApi: SpaceApi, + private val globalErrorReceiver: GlobalErrorReceiver +) : ResolveSpaceInfoTask { + override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse { + val body = SpaceSummaryParams( + maxRoomPerSpace = params.maxRoomPerSpace, + limit = params.limit, + batch = params.batchToken ?: "", + autoJoinedOnly = params.autoJoinOnly, + suggestedOnly = params.suggestedOnly + ) + return executeRequest(globalErrorReceiver) { + spaceApi.getSpaces(params.spaceId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..0fcc95fdb33e208f9ab55bcb8391f90a301ccb92 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Path + +internal interface SpaceApi { + + /** + * + * POST /_matrix/client/r0/rooms/{roomID}/spaces + * { + * "max_rooms_per_space": 5, // The maximum number of rooms/subspaces to return for a given space, if negative unbounded. default: -1. + * "auto_join_only": true, // If true, only return m.space.child events with auto_join:true, default: false, which returns all events. + * "limit": 100, // The maximum number of rooms/subspaces to return, server can override this, default: 100. + * "batch": "opaque_string" // A token to use if this is a subsequent HTTP hit, default: "". + * } + * + * Ref: + * - MSC 2946 https://github.com/matrix-org/matrix-doc/blob/kegan/spaces-summary/proposals/2946-spaces-summary.md + * - https://hackmd.io/fNYh4tjUT5mQfR1uuRzWDA + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/spaces") + suspend fun getSpaces(@Path("roomId") spaceId: String, + @Body params: SpaceSummaryParams): SpacesResponse +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..5021ff638f24ad5e92d81fc5aaa9b16ce12bf2af --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SpaceChildSummaryResponse( + /** + * The total number of state events which point to or from this room (inbound/outbound edges). + * This includes all m.space.child events in the room, in addition to m.room.parent events which point to this room as a parent. + */ + @Json(name = "num_refs") val numRefs: Int? = null, + + /** + * The room type, which is m.space for subspaces. + * It can be omitted if there is no room type in which case it should be interpreted as a normal room. + */ + @Json(name = "room_type") val roomType: String? = null, + + /** + * Aliases of the room. May be empty. + */ + @Json(name = "aliases") + val aliases: List<String>? = null, + + /** + * The canonical alias of the room, if any. + */ + @Json(name = "canonical_alias") + val canonicalAlias: String? = null, + + /** + * The name of the room, if any. + */ + @Json(name = "name") + val name: String? = null, + + /** + * Required. The number of members joined to the room. + */ + @Json(name = "num_joined_members") + val numJoinedMembers: Int = 0, + + /** + * Required. The ID of the room. + */ + @Json(name = "room_id") + val roomId: String, + + /** + * The topic of the room, if any. + */ + @Json(name = "topic") + val topic: String? = null, + + /** + * Required. Whether the room may be viewed by guest users without joining. + */ + @Json(name = "world_readable") + val worldReadable: Boolean = false, + + /** + * Required. Whether guest users may join the room and participate in it. If they can, + * they will be subject to ordinary power level rules like any other user. + */ + @Json(name = "guest_can_join") + val guestCanJoin: Boolean = false, + + /** + * The URL for the room's avatar, if one is set. + */ + @Json(name = "avatar_url") + val avatarUrl: String? = null, + + /** + * Undocumented item + */ + @Json(name = "m.federate") + val isFederated: Boolean = false +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..87425f4af2f3c0a4ae8e0854c5a6c02f13b3827d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.DefaultSpaceGetter +import org.matrix.android.sdk.internal.session.room.SpaceGetter +import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask +import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask +import retrofit2.Retrofit + +@Module +internal abstract class SpaceModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesSpacesAPI(retrofit: Retrofit): SpaceApi { + return retrofit.create(SpaceApi::class.java) + } + } + + @Binds + abstract fun bindResolveSpaceTask(task: DefaultResolveSpaceInfoTask): ResolveSpaceInfoTask + + @Binds + abstract fun bindPeekSpaceTask(task: DefaultPeekSpaceTask): PeekSpaceTask + + @Binds + abstract fun bindJoinSpaceTask(task: DefaultJoinSpaceTask): JoinSpaceTask + + @Binds + abstract fun bindSpaceGetter(getter: DefaultSpaceGetter): SpaceGetter +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..013db1c286256a277e5a8d55606a3fdbdb518a22 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SpaceSummaryParams( + /** The maximum number of rooms/subspaces to return for a given space, if negative unbounded. default: -1 */ + @Json(name = "max_rooms_per_space") val maxRoomPerSpace: Int?, + /** The maximum number of rooms/subspaces to return, server can override this, default: 100 */ + @Json(name = "limit") val limit: Int?, + /** A token to use if this is a subsequent HTTP hit, default: "". */ + @Json(name = "batch") val batch: String = "", + /** whether we should only return children with the "suggested" flag set. */ + @Json(name = "suggested_only") val suggestedOnly: Boolean?, + /** whether we should only return children with the "suggested" flag set. */ + @Json(name = "auto_join_only") val autoJoinedOnly: Boolean? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..20d63c881482c247b0c99be29dae7c43a0fff233 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class SpacesResponse( + /** Its presence indicates that there are more results to return. */ + @Json(name = "next_batch") val nextBatch: String? = null, + /** Rooms information like name/avatar/type ... */ + @Json(name = "rooms") val rooms: List<SpaceChildSummaryResponse>? = null, + /** These are the edges of the graph. The objects in the array are complete (or stripped?) m.room.parent or m.space.child events. */ + @Json(name = "events") val events: List<Event>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..f6b156a6fb167bf0a50a9f73578706d24e882f8d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space.peeking + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.api.session.space.model.SpaceChildContent +import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface PeekSpaceTask : Task<PeekSpaceTask.Params, SpacePeekResult> { + data class Params( + val roomIdOrAlias: String, + // A depth limit as a simple protection against cycles + val maxDepth: Int = 4 + ) +} + +internal class DefaultPeekSpaceTask @Inject constructor( + private val peekRoomTask: PeekRoomTask, + private val resolveRoomStateTask: ResolveRoomStateTask +) : PeekSpaceTask { + + override suspend fun execute(params: PeekSpaceTask.Params): SpacePeekResult { + val peekResult = peekRoomTask.execute(PeekRoomTask.Params(params.roomIdOrAlias)) + val roomResult = peekResult as? PeekResult.Success ?: return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult) + + // check the room type + // kind of duplicate cause we already did it in Peek? could we pass on the result?? + val stateEvents = try { + resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomResult.roomId)) + } catch (failure: Throwable) { + return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult) + } + val isSpace = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" } + ?.content + ?.toModel<RoomCreateContent>() + ?.type == RoomType.SPACE + + if (!isSpace) return SpacePeekResult.NotSpaceType(params.roomIdOrAlias) + + val children = peekChildren(stateEvents, 0, params.maxDepth) + + return SpacePeekResult.Success( + SpacePeekSummary( + params.roomIdOrAlias, + peekResult, + children + ) + ) + } + + private suspend fun peekChildren(stateEvents: List<Event>, depth: Int, maxDepth: Int): List<ISpaceChild> { + if (depth >= maxDepth) return emptyList() + val childRoomsIds = stateEvents + .filter { + it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty() + // Children where via is not present are ignored. + && it.content?.toModel<SpaceChildContent>()?.via != null + } + .map { it.stateKey to it.content?.toModel<SpaceChildContent>() } + + Timber.v("## SPACE_PEEK: found ${childRoomsIds.size} present children") + + val spaceChildResults = mutableListOf<ISpaceChild>() + childRoomsIds.forEach { entry -> + + Timber.v("## SPACE_PEEK: peeking child $entry") + // peek each child + val childId = entry.first ?: return@forEach + try { + val childPeek = peekRoomTask.execute(PeekRoomTask.Params(childId)) + + val childStateEvents = resolveRoomStateTask.execute(ResolveRoomStateTask.Params(childId)) + val createContent = childStateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" } + ?.let { it.content?.toModel<RoomCreateContent>() } + + if (!childPeek.isSuccess() || createContent == null) { + Timber.v("## SPACE_PEEK: cannot peek child $entry") + // can't peek :/ + spaceChildResults.add( + SpaceChildPeekResult( + childId, childPeek, entry.second?.autoJoin, entry.second?.order + ) + ) + // continue to next child + return@forEach + } + val type = createContent.type + if (type == RoomType.SPACE) { + Timber.v("## SPACE_PEEK: subspace child $entry") + spaceChildResults.add( + SpaceSubChildPeekResult( + childId, + childPeek, + entry.second?.autoJoin, + entry.second?.order, + peekChildren(childStateEvents, depth + 1, maxDepth) + ) + ) + } else + /** if (type == RoomType.MESSAGING || type == null)*/ + { + Timber.v("## SPACE_PEEK: room child $entry") + spaceChildResults.add( + SpaceChildPeekResult( + childId, childPeek, entry.second?.autoJoin, entry.second?.order + ) + ) + } + + // let's check child info + } catch (failure: Throwable) { + // can this happen? + Timber.e(failure, "## Failed to resolve space child") + } + } + return spaceChildResults + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..1df62e94e84e2b89eb1623b8bd9a3d6a76cda6ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space.peeking + +import org.matrix.android.sdk.api.session.room.peeking.PeekResult + +// TODO Move to api package +data class SpacePeekSummary( + val idOrAlias: String, + val roomPeekResult: PeekResult.Success, + val children: List<ISpaceChild> +) + +interface ISpaceChild { + val id: String + val roomPeekResult: PeekResult + val default: Boolean? + val order: String? +} + +data class SpaceChildPeekResult( + override val id: String, + override val roomPeekResult: PeekResult, + override val default: Boolean? = null, + override val order: String? = null +) : ISpaceChild + +data class SpaceSubChildPeekResult( + override val id: String, + override val roomPeekResult: PeekResult, + override val default: Boolean?, + override val order: String?, + val children: List<ISpaceChild> +) : ISpaceChild + +sealed class SpacePeekResult { + abstract class SpacePeekError : SpacePeekResult() + data class FailedToResolve(val spaceId: String, val roomPeekResult: PeekResult) : SpacePeekError() + data class NotSpaceType(val spaceId: String) : SpacePeekError() + + data class Success(val summary: SpacePeekSummary): SpacePeekResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt index ae60faf9057a9680c1575fd0dd31b68a7d15142f..411a9c5c065eadf238c4a8b43cede5472352fd16 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt @@ -64,7 +64,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: * @return true if the event has been decrypted */ private fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean { - Timber.v("## CRYPTO | decryptToDeviceEvent") + Timber.v("## CRYPTO | decryptToDeviceEvent") if (event.getClearType() == EventType.ENCRYPTED) { var result: MXEventDecryptionResult? = null try { @@ -76,7 +76,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: val deviceId = cryptoService.getCryptoDeviceInfo(event.senderId!!).firstOrNull { it.identityKey() == senderKey }?.deviceId ?: senderKey - Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>") + Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>") } if (null != result) { @@ -89,7 +89,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: return true } else { // should not happen - Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}") + Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt index e5d9217db71da4c0a25dda37a5bc2494688b9d51..fc1a2c387014d8f51f9068f8667023c64f1b6941 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt @@ -143,7 +143,7 @@ internal class ReadReceiptHandler @Inject constructor( @Suppress("UNCHECKED_CAST") val content = dataFromFile .events - .firstOrNull { it.type == EventType.RECEIPT } + ?.firstOrNull { it.type == EventType.RECEIPT } ?.content as? ReadReceiptContent if (content == null) { 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 2bb606e921f7536400e4f03820335c4f08a6a82a..7cebbb0192279127bb463ece2a1da75604edd24e 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 @@ -95,8 +95,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, aggregator, reporter) + + // post room sync validation +// roomSummaryUpdater.validateSpaceRelationship(realm) } + fun postSyncSpaceHierarchyHandle(realm: Realm) { + roomSummaryUpdater.validateSpaceRelationship(realm) + } // PRIVATE METHODS ***************************************************************************** private fun handleRoomSync(realm: Realm, @@ -212,6 +218,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + // Timber.v("## Space state event: $eventEntity") eventId = event.eventId root = eventEntity } @@ -455,7 +462,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { - for (event in accountData.events) { + accountData.events?.forEach { event -> val eventType = event.getClearType() if (eventType == EventType.TAG) { val content = event.getClearContent().toModel<RoomTagContent>() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 8e243c3443d9deb6b09f2479f230bc215e3b9656..157787c8cf1d4d989eb1f66edc8bd189e599989d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -132,6 +132,11 @@ internal class SyncResponseHandler @Inject constructor( Timber.v("On sync completed") cryptoSyncHandler.onSyncCompleted(syncResponse) + + // post sync stuffs + monarchy.writeAsync { + roomSyncHandler.postSyncSpaceHierarchyHandle(it) + } } /** 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 424c24663cf2ae604f30de81d8ad0145ee524aff..de8d009892fd86d793b29274e2d4b82a8e05d6f7 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 @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import com.squareup.moshi.JsonEncodingException +import kotlinx.coroutines.CancellationException import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.session.sync.SyncState @@ -199,7 +200,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) { // Timeout are not critical Timber.v("Timeout") - } else if (failure is Failure.Cancelled) { + } else if (failure is CancellationException) { Timber.v("Cancelled") } else if (failure.isTokenError()) { // No token or invalid token, stop the thread diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt index 1c35d812ee84179e1ca513da1226ad3c60c970cd..a2375507d8b7ecdb6a46e5c76b8f3864102477f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt @@ -25,5 +25,5 @@ internal data class RoomSyncAccountData( /** * List of account data events (array of Event). */ - @Json(name = "events") val events: List<Event> = emptyList() + @Json(name = "events") val events: List<Event>? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt index d59dddb3ea1e15b9676e0b21e77826fd3dce2520..f2135db6b7f949d84ce1fde3d94e73d2b9c2a83d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt @@ -26,5 +26,5 @@ internal data class RoomSyncEphemeral( /** * List of ephemeral events (array of Event). */ - @Json(name = "events") val events: List<Event> = emptyList() + @Json(name = "events") val events: List<Event>? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt index 5355b7eef1c83c6dd7fa59508551ee21931327cb..f86f05d0003fac911fe0326b2f05088595611429 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt @@ -27,5 +27,5 @@ internal data class RoomSyncState( /** * List of state events (array of Event). The resulting state corresponds to the *start* of the timeline. */ - @Json(name = "events") val events: List<Event> = emptyList() + @Json(name = "events") val events: List<Event>? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt index ddf430099a5c553810ce59da14d8d71a44b79234..27bbc4343f73e7e30263385edc3709905e2ff9a2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt @@ -27,7 +27,7 @@ internal data class RoomSyncTimeline( /** * List of events (array of Event). */ - @Json(name = "events") val events: List<Event> = emptyList(), + @Json(name = "events") val events: List<Event>? = null, /** * Boolean which tells whether there are more events on the server diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt index 0937f6d18be4255bd02d1dbcdf8ea04e36c847d4..f7664bf3c2cfc5b719bfb4a5549b0eee1c8f3101 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt @@ -17,12 +17,13 @@ package org.matrix.android.sdk.internal.session.widgets import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.widgets.WidgetURLFormatter import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.api.util.appendParamsToUrl -import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager import org.matrix.android.sdk.internal.session.widgets.token.GetScalarTokenTask @@ -37,12 +38,12 @@ internal class DefaultWidgetURLFormatter @Inject constructor(private val integra private lateinit var currentConfig: IntegrationManagerConfig private var whiteListedUrls: List<String> = emptyList() - override fun onSessionStarted() { + override fun onSessionStarted(session: Session) { setupWithConfiguration() integrationManager.addListener(this) } - override fun onSessionStopped() { + override fun onSessionStopped(session: Session) { integrationManager.removeListener(this) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt index 32442124872eaf11da5faab986e5f585a01aac4e..d741dbc966446bc957b39b0c2ad7875763d249d8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.Content @@ -34,7 +35,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource @@ -57,12 +58,12 @@ internal class WidgetManager @Inject constructor(private val integrationManager: private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) - override fun onSessionStarted() { + override fun onSessionStarted(session: Session) { lifecycleRegistry.currentState = Lifecycle.State.STARTED integrationManager.addListener(this) } - override fun onSessionStopped() { + override fun onSessionStopped(session: Session) { integrationManager.removeListener(this) lifecycleRegistry.currentState = Lifecycle.State.DESTROYED } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt index 6652628026018bc3defde3f164151ef4391646f8..bfc243c213a866b942911fd4d48efd6d48bd67d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt @@ -15,7 +15,7 @@ */ package org.matrix.android.sdk.internal.session.widgets -import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse +import org.matrix.android.sdk.api.session.openid.OpenIdToken import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -29,7 +29,7 @@ internal interface WidgetsAPI { * @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961) */ @POST("register") - suspend fun register(@Body body: RequestOpenIdTokenResponse, + suspend fun register(@Body body: OpenIdToken, @Query("v") version: String?): RegisterWidgetResponse @GET("account") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt index 97f9a0dd51330c6291bc47130fbf232c61d0b8e4..bc80cf7ee8bd3fd7af38f5f6d07b16a098f264ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt @@ -37,7 +37,8 @@ internal data class ConfigurableTask<PARAMS, RESULT>( val id: UUID, val callbackThread: TaskThread, val executionThread: TaskThread, - val callback: MatrixCallback<RESULT> + val callback: MatrixCallback<RESULT>, + val maxRetryCount: Int = 0 ) : Task<PARAMS, RESULT> by task { @@ -57,7 +58,8 @@ internal data class ConfigurableTask<PARAMS, RESULT>( id = id, callbackThread = callbackThread, executionThread = executionThread, - callback = callback + callback = callback, + maxRetryCount = retryCount ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt index a6c80a0b1a82540f1f3971b900b0d41e49e9d0ff..a5d031e02ae2b6f5e408659771b1cc8667b49487 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt @@ -16,7 +16,29 @@ package org.matrix.android.sdk.internal.task +import kotlinx.coroutines.delay +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.shouldBeRetried +import timber.log.Timber + internal interface Task<PARAMS, RESULT> { suspend fun execute(params: PARAMS): RESULT + + suspend fun executeRetry(params: PARAMS, remainingRetry: Int) : RESULT { + return try { + execute(params) + } catch (failure: Throwable) { + if (failure.shouldBeRetried() && remainingRetry > 0) { + Timber.d(failure, "## TASK: Retriable error") + if (failure is Failure.ServerError) { + val waitTime = failure.error.retryAfterMillis ?: 0L + Timber.d(failure, "## TASK: Quota wait time $waitTime") + delay(waitTime + 100) + } + return executeRetry(params, remainingRetry - 1) + } + throw failure + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt index 478a356432543b42be9623ff6af436aa3449dd52..4da16eff226e81d4611ea53412746d47098aa222 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt @@ -40,9 +40,9 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers .launch(task.callbackThread.toDispatcher()) { val resultOrFailure = runCatching { withContext(task.executionThread.toDispatcher()) { - Timber.v("Enqueue task $task") - Timber.v("Execute task $task on ${Thread.currentThread().name}") - task.execute(task.params) + Timber.v("## TASK: Enqueue task $task") + Timber.v("## TASK: Execute task $task on ${Thread.currentThread().name}") + task.executeRetry(task.params, task.maxRetryCount) } } resultOrFailure diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FailureExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FailureExt.kt new file mode 100644 index 0000000000000000000000000000000000000000..8c78feeac3d883d6d2ab668946a4254e1c86cc49 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FailureExt.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Try to extract and serialize a MatrixError, or default to localizedMessage + */ +internal fun Throwable.toMatrixErrorStr(): String { + return (this as? Failure.ServerError) + ?.let { + // Serialize the MatrixError in this case + val adapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java) + tryOrNull { adapter.toJson(error) } + } + ?: localizedMessage + ?: "error" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt index e19b1bcca7c7ad625101efcb9121f95279bf471a..47f20913ecaac069dc8eb7bbb502faeaf6019fc6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt @@ -27,7 +27,7 @@ fun String.md5() = try { digest.update(toByteArray()) digest.digest() .joinToString("") { String.format("%02X", it) } - .toLowerCase(Locale.ROOT) + .lowercase(Locale.ROOT) } catch (exc: Exception) { // Should not happen, but just in case hashCode().toString() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt index 2fabca4be86e5f7a66c237788d3992fde700452f..aa0b92aa45806a83cc365ea4ddd137e0eea83444 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.util import timber.log.Timber +import java.util.Locale /** * Convert a string to an UTF8 String @@ -24,7 +25,7 @@ import timber.log.Timber * @param s the string to convert * @return the utf-8 string */ -fun convertToUTF8(s: String): String { +internal fun convertToUTF8(s: String): String { return try { val bytes = s.toByteArray(Charsets.UTF_8) String(bytes) @@ -40,7 +41,7 @@ fun convertToUTF8(s: String): String { * @param s the string to convert * @return the utf-16 string */ -fun convertFromUTF8(s: String): String { +internal fun convertFromUTF8(s: String): String { return try { val bytes = s.toByteArray() String(bytes, Charsets.UTF_8) @@ -56,7 +57,7 @@ fun convertFromUTF8(s: String): String { * @param subString the string to search for * @return whether a match was found */ -fun String.caseInsensitiveFind(subString: String): Boolean { +internal fun String.caseInsensitiveFind(subString: String): Boolean { // add sanity checks if (subString.isEmpty() || isEmpty()) { return false @@ -78,3 +79,14 @@ internal val spaceChars = "[\u00A0\u2000-\u200B\u2800\u3000]".toRegex() * Strip all the UTF-8 chars which are actually spaces */ internal fun String.replaceSpaceChars() = replace(spaceChars, "") + +// String.capitalize is now deprecated +internal fun String.safeCapitalize(): String { + return replaceFirstChar { char -> + if (char.isLowerCase()) { + char.titlecase(Locale.getDefault()) + } else { + char.toString() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/TemporaryFileCreator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/TemporaryFileCreator.kt new file mode 100644 index 0000000000000000000000000000000000000000..2790ffba36f0d982ca4183106d010162b6e6de63 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/TemporaryFileCreator.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import android.content.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.util.UUID +import javax.inject.Inject + +internal class TemporaryFileCreator @Inject constructor( + private val context: Context +) { + suspend fun create(): File { + return withContext(Dispatchers.IO) { + File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..b660796ad82d483d993e815171f418bd60d27b0c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util.system + +internal interface BuildVersionSdkIntProvider { + /** + * Return the current version of the Android SDK + */ + fun get(): Int +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..d9f0064f38f984ab2dc3d5e3a576f67c261f5aa0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util.system + +import android.os.Build +import javax.inject.Inject + +internal class DefaultBuildVersionSdkIntProvider @Inject constructor() + : BuildVersionSdkIntProvider { + override fun get() = Build.VERSION.SDK_INT +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a7b50175a2978335d76e83f58a32c5877b7865c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util.system + +import dagger.Binds +import dagger.Module + +@Module +internal abstract class SystemModule { + + @Binds + abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/util/GraphUtilsTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/util/GraphUtilsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..d28192a2820172f2ba72472efbe84584ff7aab07 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/util/GraphUtilsTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.MatrixTest +import org.matrix.android.sdk.internal.session.room.summary.Graph + +@FixMethodOrder(MethodSorters.JVM) +class GraphUtilsTest : MatrixTest { + + @Test + fun testCreateGraph() { + val graph = Graph() + + graph.addEdge("E", "C") + graph.addEdge("B", "A") + graph.addEdge("C", "A") + graph.addEdge("D", "C") + graph.addEdge("E", "D") + + graph.getOrCreateNode("F") + + System.out.println(graph.toString()) + + val backEdges = graph.findBackwardEdges(graph.getOrCreateNode("E")) + + assertTrue("There should not be any cycle in this graphs", backEdges.isEmpty()) + } + + @Test + fun testCycleGraph() { + val graph = Graph() + + graph.addEdge("E", "C") + graph.addEdge("B", "A") + graph.addEdge("C", "A") + graph.addEdge("D", "C") + graph.addEdge("E", "D") + + graph.getOrCreateNode("F") + + // adding loops + graph.addEdge("C", "E") + graph.addEdge("B", "B") + + System.out.println(graph.toString()) + + val backEdges = graph.findBackwardEdges(graph.getOrCreateNode("E")) + System.out.println(backEdges.joinToString(" | ") { "${it.source.name} -> ${it.destination.name}" }) + + assertEquals("There should be 2 backward edges not ${backEdges.size}", 2, backEdges.size) + + val edge1 = backEdges.find { it.source.name == "C" } + assertNotNull("There should be a back edge from C", edge1) + assertEquals("There should be a back edge C -> E", "E", edge1!!.destination.name) + + val edge2 = backEdges.find { it.source.name == "B" } + assertNotNull("There should be a back edge from B", edge2) + assertEquals("There should be a back edge C -> C", "B", edge2!!.destination.name) + + // clean the graph + val acyclicGraph = graph.withoutEdges(backEdges) + System.out.println(acyclicGraph.toString()) + + assertTrue("There should be no backward edges", acyclicGraph.findBackwardEdges(acyclicGraph.getOrCreateNode("E")).isEmpty()) + + val flatten = acyclicGraph.flattenDestination() + + assertTrue(flatten[acyclicGraph.getOrCreateNode("A")]!!.isEmpty()) + + val flattenParentsB = flatten[acyclicGraph.getOrCreateNode("B")] + assertTrue(flattenParentsB!!.size == 1) + assertTrue(flattenParentsB.contains(acyclicGraph.getOrCreateNode("A"))) + + val flattenParentsE = flatten[acyclicGraph.getOrCreateNode("E")] + assertEquals(3, flattenParentsE!!.size) + assertTrue(flattenParentsE.contains(acyclicGraph.getOrCreateNode("A"))) + assertTrue(flattenParentsE.contains(acyclicGraph.getOrCreateNode("C"))) + assertTrue(flattenParentsE.contains(acyclicGraph.getOrCreateNode("D"))) + +// System.out.println( +// buildString { +// flatten.entries.forEach { +// append("${it.key.name}: [") +// append(it.value.joinToString(",") { it.name }) +// append("]\n") +// } +// } +// ) + } +} diff --git a/tools/import_from_element.sh b/tools/import_from_element.sh index 36a1646dc6bd755a522a451454b4047a8aa620a5..331df43c2d9c244266a5532f99b0478a0032d065 100755 --- a/tools/import_from_element.sh +++ b/tools/import_from_element.sh @@ -39,6 +39,8 @@ cp -r ${elementAndroidPath}/matrix-sdk-android . # Add all changes to git git add -A +read -p "Press enter to build the library (update the version first?)" + # Build the library ./gradlew clean assembleRelease