Merge branch 'spaces' into sc

Change-Id: Ia2a25202fab4e5be3c4800bb5cb6ab47fc828bac
This commit is contained in:
SpiritCroc 2021-05-12 15:44:01 +02:00
commit 2269e0f1ed
462 changed files with 21809 additions and 2008 deletions

View file

@ -1,3 +1,47 @@
Changes in Element 1.1.7 (2021-XX-XX)
===================================================
Features ✨:
- Spaces beta
Improvements 🙌:
- Add ability to install APK from directly from Element (#2381)
- Delete and react to stickers (#3250)
Bugfix 🐛:
- Message states cosmetic changes (#3007)
- Fix exception in rxSingle (#3180)
- Do not invite the current user when creating a room (#3123)
- Fix color issues when the system theme is changed (#2738)
- Fix issues on Android 11 (#3067)
- Fix issue when opening encrypted files (#3186)
- Fix wording issue (#3242)
- Fix missing sender information after edits (#3184)
Translations 🗣:
-
SDK API changes ⚠️:
- RegistrationWizard.createAccount() parameters are now all optional, following Matrix spec (#3205)
Build 🧱:
- Upgrade to gradle 7
Test:
-
Other changes:
- New store descriptions
- `master` branch has been renamed to `main`. To apply change to your dev environment, run:
```sh
git branch -m master main
git fetch origin
git branch -u origin/main main
# And optionally
git remote prune origin
```
- Allow cleartext (non-SSL) connections to Matrix servers on LAN hosts (#3166)
Changes in Element 1.1.6 (2021-04-16)
===================================================

View file

@ -69,7 +69,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.recyclerview:recyclerview:1.2.0-rc01"
implementation "androidx.recyclerview:recyclerview:1.2.0"
implementation 'com.google.android.material:material:1.3.0'
}

View file

@ -16,8 +16,8 @@ buildscript {
classpath 'com.google.gms:google-services:4.3.5'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1.1'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.3'
classpath "com.likethesalad.android:string-reference:1.2.1"
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4'
classpath "com.likethesalad.android:string-reference:1.2.2"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -51,7 +51,7 @@ allprojects {
}
}
maven {
url "http://dl.bintray.com/piasy/maven"
url "https://dl.bintray.com/piasy/maven"
content {
includeGroupByRegex "com\\.github\\.piasy"
}
@ -59,7 +59,7 @@ allprojects {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
// Jitsi repo
maven {
url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.1.0"
url "https://github.com/vector-im/jitsi_libre_maven/raw/main/android-sdk-3.1.0"
// Note: to test Jitsi release you can use a local file like this:
// url "file:///Users/bmarty/workspaces/jitsi_libre_maven/android-sdk-3.1.0"
}

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=9af5c8e7e2cd1a3b0f694a4ac262b9f38c75262e74a9e8b5101af302a6beadd7
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip
distributionSha256Sum=81003f83b0056d20eedf48cddd4f52a9813163d4ba185bcf8abd34b8eeea4cbd
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View file

@ -1,2 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.matrix.android.sdk.rx" />
<manifest package="org.matrix.android.sdk.rx" />

View file

@ -1,56 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.rx
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.util.Cancelable
import io.reactivex.Completable
import io.reactivex.Single
fun <T> singleBuilder(builder: (MatrixCallback<T>) -> Cancelable): Single<T> = Single.create { emitter ->
val callback = object : MatrixCallback<T> {
override fun onSuccess(data: T) {
// Add `!!` to fix the warning:
// "Type mismatch: type parameter with nullable bounds is used T is used where T was expected. This warning will become an error soon"
emitter.onSuccess(data!!)
}
override fun onFailure(failure: Throwable) {
emitter.tryOnError(failure)
}
}
val cancelable = builder(callback)
emitter.setCancellable {
cancelable.cancel()
}
}
fun <T> completableBuilder(builder: (MatrixCallback<T>) -> Cancelable): Completable = Completable.create { emitter ->
val callback = object : MatrixCallback<T> {
override fun onSuccess(data: T) {
emitter.onComplete()
}
override fun onFailure(failure: Throwable) {
emitter.tryOnError(failure)
}
}
val cancelable = builder(callback)
emitter.setCancellable {
cancelable.cancel()
}
}

View file

@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
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.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.session.sync.SyncState
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.session.widgets.model.Widget
@ -66,6 +67,13 @@ class RxSession(private val session: Session) {
}
}
fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable<List<RoomSummary>> {
return session.spaceService().getSpaceSummariesLive(queryParams).asObservable()
.startWithCallable {
session.spaceService().getSpaceSummaries(queryParams)
}
}
fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
return session.getBreadcrumbsLive(queryParams).asObservable()
.startWithCallable {
@ -124,8 +132,8 @@ class RxSession(private val session: Session) {
.startWithCallable { session.getPendingThreePids() }
}
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
session.createRoom(roomParams, it)
fun createRoom(roomParams: CreateRoomParams): Single<String> = rxSingle {
session.createRoom(roomParams)
}
fun searchUsersDirectory(search: String,
@ -136,13 +144,13 @@ class RxSession(private val session: Session) {
fun joinRoom(roomIdOrAlias: String,
reason: String? = null,
viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
session.joinRoom(roomIdOrAlias, reason, viaServers, it)
viaServers: List<String> = emptyList()): Single<Unit> = rxSingle {
session.joinRoom(roomIdOrAlias, reason, viaServers)
}
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean): Single<Optional<RoomAliasDescription>> = singleBuilder {
session.getRoomIdByAlias(roomAlias, searchOnServer, it)
searchOnServer: Boolean): Single<Optional<RoomAliasDescription>> = rxSingle {
session.getRoomIdByAlias(roomAlias, searchOnServer)
}
fun getProfileInfo(userId: String): Single<JsonDict> = rxSingle {

View file

@ -115,7 +115,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'
def work_version = '2.5.0'
def retrofit_version = '2.9.0'
@ -169,7 +169,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.21'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.22'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.5.1'

View file

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

View file

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

View file

@ -0,0 +1,194 @@
/*
* 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.delay
import kotlinx.coroutines.runBlocking
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.QueryStringValue
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
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@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"
val spaceId: String
runBlocking {
spaceId = session.spaceService().createSpace(roomName, topic, null, true)
// wait a bit to let the summary update it self :/
delay(400)
}
val syncedSpace = session.spaceService().getSpace(spaceId)
assertEquals(roomName, syncedSpace?.asRoom()?.roomSummary()?.name, "Room name should be set")
assertEquals(topic, syncedSpace?.asRoom()?.roomSummary()?.topic, "Room topic should be set")
// assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set")
assertNotNull(syncedSpace, "Space should be found by Id")
val creationEvent = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_CREATE)
val createContent = creationEvent?.content.toModel<RoomCreateContent>()
assertEquals(RoomType.SPACE, createContent?.type, "Room type should be space")
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(100, powerLevelsContent?.eventsDefault, "Space-rooms should be created with a power level for events_default of 100")
val guestAccess = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_GUEST_ACCESS)?.content
?.toModel<RoomGuestAccessContent>()?.guestAccess
assertEquals(GuestAccess.CanJoin, guestAccess, "Public space room should be peekable by guest")
val historyVisibility = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY)?.content
?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility
assertEquals(RoomHistoryVisibility.WORLD_READABLE, historyVisibility, "Public space room should be world readable")
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(roomName, spaceBobPov?.asRoom()?.roomSummary()?.name, "Room name should be set")
assertEquals(topic, spaceBobPov?.asRoom()?.roomSummary()?.topic, "Room topic should be set")
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
val firstChild: String = runBlocking {
aliceSession.createRoom(CreateRoomParams().apply {
this.name = "FirstRoom"
this.topic = "Description of first room"
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
})
}
runBlocking {
syncedSpace?.addChildren(firstChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", true)
}
val secondChild: String = runBlocking {
aliceSession.createRoom(CreateRoomParams().apply {
this.name = "SecondRoom"
this.topic = "Description of second room"
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
})
}
runBlocking {
syncedSpace?.addChildren(secondChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", false)
}
// Try to join from bob, it's a public space no need to invite
val joinResult = runBlocking {
bobSession.spaceService().joinSpace(spaceId)
}
assertEquals(JoinSpaceResult.Success, joinResult)
val spaceBobPov = bobSession.spaceService().getSpace(spaceId)
assertEquals(roomName, spaceBobPov?.asRoom()?.roomSummary()?.name, "Room name should be set")
assertEquals(topic, spaceBobPov?.asRoom()?.roomSummary()?.topic, "Room topic should be set")
// check if bob has joined automatically the first room
val bobMembershipFirstRoom = bobSession.getRoom(firstChild)?.roomSummary()?.membership
assertEquals(Membership.JOIN, bobMembershipFirstRoom, "Bob should have joined this room")
RoomSummaryQueryParams.Builder()
val spaceSummaryBobPov = bobSession.spaceService().getSpaceSummaries(roomSummaryQueryParams {
this.roomId = QueryStringValue.Equals(spaceId)
this.memberships = listOf(Membership.JOIN)
}).firstOrNull()
assertEquals(2, spaceSummaryBobPov?.spaceChildren?.size ?: -1, "Unexpected number of children")
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(bobSession)
}
}

View file

@ -0,0 +1,414 @@
/*
* 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.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
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.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
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@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"
val spaceId: String
runBlocking {
spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
// wait a bit to let the summary update it self :/
delay(400)
}
val syncedSpace = session.spaceService().getSpace(spaceId)
val roomId = runBlocking {
session.createRoom(CreateRoomParams().apply { name = "General" })
}
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
runBlocking {
syncedSpace!!.addChildren(roomId, viaServers, null, true)
}
runBlocking {
session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers)
}
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")
val spaceId = runBlocking {
session.spaceService().createSpace(spaceName, topic, null, true)
}
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 viaServers = listOf(session.sessionParams.homeServerHost ?: "")
runBlocking {
roomIdList.forEach { entry ->
syncedSpace!!.addChildren(entry.first, viaServers, null, true)
}
}
runBlocking {
roomIdList.forEach {
session.spaceService().setSpaceParent(it.first, spaceId, it.second, viaServers)
}
delay(400)
}
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(1, parents.size, "Unexpected number of parent")
assertEquals(spaceName, parents.first().roomSummary?.name, "Unexpected parent name ")
assertEquals(if (it.second) 1 else 0, canonicalParents?.size ?: 0, "Parent of ${it.first} should be canonical ${it.second}")
}
}
@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 ?: "")
runBlocking {
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
}
// Create orphan rooms
val orphan1 = runBlocking {
session.createRoom(CreateRoomParams().apply { name = "O1" })
}
val orphan2 = runBlocking {
session.createRoom(CreateRoomParams().apply { name = "O2" })
}
val allRooms = session.getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) })
assertEquals(9, allRooms.size, "Unexpected number of rooms")
val orphans = session.getFlattenRoomSummaryChildrenOf(null)
assertEquals(2, orphans.size, "Unexpected number of orphan rooms")
assertTrue(orphans.indexOfFirst { it.roomId == orphan1 } != -1, "O1 should be an orphan")
assertTrue(orphans.indexOfFirst { it.roomId == orphan2 } != -1, "O2 should be an orphan ${orphans.map { it.name }}")
val aChildren = session.getFlattenRoomSummaryChildrenOf(spaceAInfo.spaceId)
assertEquals(4, aChildren.size, "Unexpected number of flatten child rooms")
assertTrue(aChildren.indexOfFirst { it.name == "A1" } != -1, "A1 should be a child of A")
assertTrue(aChildren.indexOfFirst { it.name == "A2" } != -1, "A2 should be a child of A")
assertTrue(aChildren.indexOfFirst { it.name == "C1" } != -1, "CA should be a grand child of A")
assertTrue(aChildren.indexOfFirst { it.name == "C2" } != -1, "A1 should be a grand child of A")
// Add a non canonical child and check that it does not appear as orphan
val a3 = runBlocking {
session.createRoom(CreateRoomParams().apply { name = "A3" })
}
runBlocking {
spaceA!!.addChildren(a3, viaServers, null, false)
delay(400)
// here we do not set the parent!!
}
val orphansUpdate = session.getFlattenRoomSummaryChildrenOf(null)
assertEquals(2, orphansUpdate.size, "Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}")
}
@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 ?: "")
runBlocking {
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
}
// add back A as subspace of C
runBlocking {
val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId)
spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
}
Thread.sleep(1000)
// A -> C -> A
val aChildren = session.getFlattenRoomSummaryChildrenOf(spaceAInfo.spaceId)
assertEquals(4, aChildren.size, "Unexpected number of flatten child rooms ${aChildren.map { it.name }}")
assertTrue(aChildren.indexOfFirst { it.name == "A1" } != -1, "A1 should be a child of A")
assertTrue(aChildren.indexOfFirst { it.name == "A2" } != -1, "A2 should be a child of A")
assertTrue(aChildren.indexOfFirst { it.name == "C1" } != -1, "CA should be a grand child of A")
assertTrue(aChildren.indexOfFirst { it.name == "C2" } != -1, "A1 should be a grand child of A")
}
@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?.indexOfFirst { it.name == "C1" } != -1
&& children?.indexOfFirst { it.name == "C2" } != -1
) {
// 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 {
val spaceId = runBlocking {
session.spaceService().createSpace(spaceName, "Test Topic", null, true)
}
val syncedSpace = session.spaceService().getSpace(spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
val roomIds =
childInfo.map { entry ->
runBlocking {
session.createRoom(CreateRoomParams().apply { name = entry.first })
}
}
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(2, rootSpaces.size, "Unexpected number of root spaces ${rootSpaces.map { it.name }}")
}
}

View file

@ -16,12 +16,10 @@
package org.matrix.android.sdk.api.auth.data
sealed class LoginFlowResult {
data class Success(
data class LoginFlowResult(
val supportedLoginTypes: List<String>,
val ssoIdentityProviders: List<SsoIdentityProvider>?,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String,
val isOutdatedHomeserver: Boolean
) : LoginFlowResult()
}
)

View file

@ -20,7 +20,9 @@ interface RegistrationWizard {
suspend fun getRegistrationFlow(): RegistrationResult
suspend fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?): RegistrationResult
suspend fun createAccount(userName: String?,
password: String?,
initialDeviceDisplayName: String?): RegistrationResult
suspend fun performReCaptcha(response: String): RegistrationResult

View file

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

View file

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

View file

@ -48,6 +48,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 +228,11 @@ interface Session :
*/
fun thirdPartyService(): ThirdPartyService
/**
* Returns the space service associated with the session
*/
fun spaceService(): SpaceService
/**
* Add a listener to the session.
* @param listener the listener to add.
@ -249,13 +255,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

View file

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

View file

@ -53,6 +53,12 @@ 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_CHILD = "org.matrix.msc1772.space.child"
// const val STATE_SPACE_PARENT = "m.space.parent"
const val STATE_SPACE_PARENT = "org.matrix.msc1772.space.parent"
/**
* Note that this Event has been deprecated, see
* - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
@ -75,6 +81,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"

View file

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

View file

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

View file

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

View file

@ -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,
suspend fun joinRoom(roomIdOrAlias: String,
reason: String? = null,
viaServers: List<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable
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>>
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2019 New Vector Ltd
* 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.
@ -12,14 +12,12 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.app.features.grouplist
package org.matrix.android.sdk.api.session.room
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.group.model.GroupSummary
sealed class GroupListAction : VectorViewModelAction {
data class SelectGroup(val groupSummary: GroupSummary) : GroupListAction()
enum class RoomSortOrder {
NAME,
ACTIVITY,
NONE
}

View file

@ -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 activeSpaceId: 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 activeSpaceId: 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,
activeSpaceId = activeSpaceId,
activeGroupId = activeGroupId
)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,35 +16,31 @@
package org.matrix.android.sdk.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Ref: https://matrix.org/docs/spec/client_server/latest#room-history-visibility
*/
@JsonClass(generateAdapter = false)
enum class RoomHistoryVisibility {
/**
* All events while this is the m.room.history_visibility value may be shared by any
* participating homeserver with anyone, regardless of whether they have ever joined the room.
*/
@Json(name = "world_readable") WORLD_READABLE,
WORLD_READABLE,
/**
* Previous events are always accessible to newly joined members. All events in the
* room are accessible, even those sent when the member was not a part of the room.
*/
@Json(name = "shared") SHARED,
SHARED,
/**
* Events are accessible to newly joined members from the point they were invited onwards.
* Events stop being accessible when the member's state changes to something other than invite or join.
*/
@Json(name = "invited") INVITED,
INVITED,
/**
* Events are accessible to newly joined members from the point they joined the room onwards.
* Events stop being accessible when the member's state changes to something other than join.
*/
@Json(name = "joined") JOINED
JOINED
}

View file

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

View file

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

View file

@ -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,13 +27,18 @@ 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
"private" -> RoomJoinRules.PRIVATE
"restricted" -> RoomJoinRules.RESTRICTED
else -> {
Timber.w("Invalid value for RoomJoinRules: `$_joinRules`")
null

View file

@ -37,6 +37,7 @@ data class RoomSummary constructor(
val canonicalAlias: String? = null,
val aliases: List<String> = emptyList(),
val isDirect: Boolean = false,
val directUserId: String? = null,
val joinedMembersCount: Int? = 0,
val invitedMembersCount: Int? = 0,
val latestPreviewableEvent: TimelineEvent? = null,
@ -48,6 +49,7 @@ data class RoomSummary constructor(
val hasUnreadMessages: Boolean = false,
val hasUnreadContentMessages: Boolean = false,
val hasUnreadOriginalContentMessages: Boolean = false,
val unreadCount: Int? = 0,
val markedUnread: Boolean = false,
val tags: List<RoomTag> = emptyList(),
val membership: Membership = Membership.NONE,
@ -60,7 +62,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

View file

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

View file

@ -0,0 +1,23 @@
/*
* 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 = "org.matrix.msc1772.space" // "m.space"
// const val MESSAGING = "org.matrix.msc1840.messaging"
}

View file

@ -0,0 +1,33 @@
/*
* 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?
)

View file

@ -1,5 +1,5 @@
/*
* Copyright 2019 New Vector Ltd
* 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.
@ -14,14 +14,11 @@
* limitations under the License.
*/
package im.vector.app.features.grouplist
package org.matrix.android.sdk.api.session.room.model
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.group.model.GroupSummary
data class GroupListViewState(
val asyncGroups: Async<List<GroupSummary>> = Uninitialized,
val selectedGroup: GroupSummary? = null
) : MvRxState
data class SpaceParentInfo(
val parentId: String?,
val roomSummary: RoomSummary?,
val canonical: Boolean?,
val viaServers: List<String>
)

View file

@ -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 = "org.matrix.msc1772.type"
}
}

View file

@ -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 = "org.matrix.msc1772.type") val type: String? = null
)

View file

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

View file

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

View file

@ -22,7 +22,7 @@ data class RoomAggregateNotificationCount(
val unreadCount: Int,
val markedUnreadCount: Int
) {
val totalCount = notificationCount + highlightCount + markedUnreadCount
val totalCount = notificationCount + markedUnreadCount
val isHighlight = highlightCount > 0
val markedUnread = markedUnreadCount > 0
}

View file

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

View file

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

View file

@ -0,0 +1,50 @@
/*
* 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)
// fun getChildren() : List<IRoomSummary>
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -144,7 +144,6 @@ 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(
@ -153,7 +152,6 @@ internal class DefaultAuthenticationService @Inject constructor(
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()
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,7 +19,10 @@ 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.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
@ -31,6 +34,9 @@ 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.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields
import org.matrix.android.sdk.internal.di.MoshiProvider
import timber.log.Timber
import javax.inject.Inject
@ -39,10 +45,10 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object {
// SC-specific DB changes on top of Element
// 1: added markedUnread field
const val SESSION_STORE_SCHEMA_SC_VERSION = 1L
const val SESSION_STORE_SCHEMA_SC_VERSION = 2L
const val SESSION_STORE_SCHEMA_SC_VERSION_OFFSET = (1L shl 12)
const val SESSION_STORE_SCHEMA_VERSION = 9L +
const val SESSION_STORE_SCHEMA_VERSION = 10L +
SESSION_STORE_SCHEMA_SC_VERSION * SESSION_STORE_SCHEMA_SC_VERSION_OFFSET
}
@ -61,8 +67,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 (oldScVersion <= 0) migrateToSc1(realm)
if (oldScVersion <= 1) migrateToSc2(realm)
}
// SC Version 1L added markedUnread
@ -72,6 +80,13 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
?.addField(RoomSummaryEntityFields.MARKED_UNREAD, Boolean::class.java)
}
// SC Version 2L added unreadCount
private fun migrateToSc2(realm: DynamicRealm) {
Timber.d("Step SC 1 -> 2")
realm.schema.get("RoomSummaryEntity")
?.addField(RoomSummaryEntityFields.UNREAD_COUNT, Int::class.java)
}
private fun migrateTo1(realm: DynamicRealm) {
@ -194,7 +209,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
}
@ -214,4 +228,44 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
}
}
}
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")!!)
}
}

View file

@ -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
@ -49,6 +51,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
topic = roomSummaryEntity.topic ?: "",
avatarUrl = roomSummaryEntity.avatarUrl ?: "",
isDirect = roomSummaryEntity.isDirect,
directUserId = roomSummaryEntity.directUserId,
latestPreviewableEvent = latestEvent,
latestPreviewableContentEvent = latestContentEvent,
latestPreviewableOriginalContentEvent = latestOriginalContentEvent,
@ -74,7 +77,32 @@ 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
)
},
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList()
)
}
}

View file

@ -43,6 +43,5 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
set(value) {
membersLoadStatusStr = value.name
}
companion object
}

View file

@ -27,7 +27,10 @@ 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? = ""
@ -103,6 +106,17 @@ internal open class RoomSummaryEntity(
if (value != field) field = value
}
var unreadCount: Int = 0
set(value) {
if (value != field) field = value
}
get() {
if (field == 0 && hasUnreadOriginalContentMessages) {
return 1
}
return field
}
var readMarkerId: String? = null
set(value) {
if (value != field) field = value
@ -229,6 +243,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
@ -269,6 +293,5 @@ internal open class RoomSummaryEntity(
roomEncryptionTrustLevelStr = value?.name
}
}
companion object
}

View file

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

View file

@ -0,0 +1,44 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.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 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
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,6 +40,7 @@ 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.permalinks.PermalinkService
@ -49,6 +52,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 +124,7 @@ internal class DefaultSession @Inject constructor(
private val integrationManagerService: IntegrationManagerService,
private val thirdPartyService: Lazy<ThirdPartyService>,
private val callSignalingService: Lazy<CallSignalingService>,
private val spaceService: Lazy<SpaceService>,
@UnauthenticatedWithCertificate
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
) : Session,
@ -159,7 +164,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 +210,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 +238,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()
}
workManagerProvider.cancelAllWorks()
}
override fun onGlobalError(globalError: GlobalError) {
sessionListeners.dispatchGlobalError(globalError)
sessionListeners.dispatch {
it.onGlobalError(this, globalError)
}
}
override fun contentUrlResolver() = contentUrlResolver
@ -265,6 +287,8 @@ internal class DefaultSession @Inject constructor(
override fun thirdPartyService(): ThirdPartyService = thirdPartyService.get()
override fun spaceService(): SpaceService = spaceService.get()
override fun getOkHttpClient(): OkHttpClient {
return unauthenticatedWithCertificateOkHttpClient.get()
}

View file

@ -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
@ -91,7 +92,8 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
FederationModule::class,
CallModule::class,
SearchModule::class,
ThirdPartyModule::class
ThirdPartyModule::class,
SpaceModule::class
]
)
@SessionScope

View file

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

View file

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

View file

@ -33,6 +33,7 @@ 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
@ -343,6 +344,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

View file

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

View file

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

View file

@ -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,13 @@ 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.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
@ -148,4 +151,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)
}
}

View file

@ -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, preferenceProvider: RoomSummary.RoomSummaryPreferenceProvider): 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
override suspend fun peekRoom(roomIdOrAlias: String): PeekResult {
return peekRoomTask.execute(PeekRoomTask.Params(roomIdOrAlias))
}
.executeBy(taskExecutor)
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)
}
}

View file

@ -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
@ -91,6 +92,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
@ -137,6 +139,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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
@ -80,11 +91,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) },
@ -106,10 +166,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)
@ -120,30 +180,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
}
}
@ -197,6 +275,162 @@ 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.beginGroup()
.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0).or()
.equalTo(RoomSummaryEntityFields.MARKED_UNREAD, true).endGroup()
RoomCategoryFilter.ALL -> Unit // nop
}
// Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}")
when (queryParams.activeSpaceId) {
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.activeSpaceId.currentSpaceId == null) {
// orphan rooms
query.isNull(RoomSummaryEntityFields.FLATTEN_PARENT_IDS)
} else {
query.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceId.currentSpaceId)
}
}
is ActiveSpaceFilter.ExcludeSpace -> {
query.not().contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceId.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)
}
}
}
}
}
}
}

View file

@ -17,6 +17,7 @@
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
@ -25,6 +26,8 @@ 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.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 +37,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 +103,11 @@ 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 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)
@ -177,4 +196,238 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.latestPreviewableContentEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
roomSummaryEntity.latestPreviewableOriginalContentEvent = RoomSummaryEventsHelper.getLatestPreviewableEventScOriginalContent(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.isEmpty()) {
dmRoom.flattenParentIds = null
} else {
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
var unreadCount = 0
var markedUnreadCount = 0
realm.where(RoomSummaryEntity::class.java)
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, listOf(Membership.JOIN))
.notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE)
.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, space.roomId)
.findAll().forEach {
highlightCount += it.highlightCount
notificationCount += it.notificationCount
unreadCount += if (it.hasUnreadOriginalContentMessages) 1 else 0
markedUnreadCount += if (it.markedUnread) 1 else 0
}
space.highlightCount = highlightCount
space.notificationCount = notificationCount + markedUnreadCount
space.unreadCount = unreadCount
}
// 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 {
//
// }
}

View file

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

View file

@ -0,0 +1,119 @@
/*
* 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.room.summary.RoomSummaryDataSource
internal class DefaultSpace(
private val room: Room,
private val spaceSummaryDataSource: RoomSummaryDataSource
) : 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?) {
room.sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD,
stateKey = roomId,
body = SpaceChildContent(
via = viaServers,
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 = existing.order,
via = null,
autoJoin = existing.autoJoin
).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
).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")
// 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
).toContent()
)
}
}

View file

@ -0,0 +1,199 @@
/*
* 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
enableEncryption()
}
})
}
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 }
?.map { childSummary ->
val childStateEv = response.events
?.firstOrNull { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
val childStateEvContent = childStateEv?.content.toModel<SpaceChildContent>()
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 ?: emptyList(),
activeMemberCount = childSummary.numJoinedMembers,
parentRoomId = childStateEv?.roomId
)
}.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()
)
}
}

View file

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

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