Quick sync group management (WIP)

This commit is contained in:
ganfra 2018-11-05 17:39:07 +01:00
parent 3199f5dcd6
commit a3539153ef
30 changed files with 582 additions and 6 deletions

View File

@ -1,6 +1,7 @@
package im.vector.matrix.rx

import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import io.reactivex.Observable

@ -10,6 +11,10 @@ class RxSession(private val session: Session) {
return session.liveRoomSummaries().asObservable()
}

fun liveGroupSummaries(): Observable<List<GroupSummary>> {
return session.liveGroupSummaries().asObservable()
}

}

fun Session.rx(): RxSession {

View File

@ -1,10 +1,11 @@
package im.vector.matrix.android.api.session

import android.support.annotation.MainThread
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.internal.auth.data.SessionParams

interface Session : RoomService {
interface Session : RoomService, GroupService {

val sessionParams: SessionParams


View File

@ -0,0 +1,5 @@
package im.vector.matrix.android.api.session.group

interface Group {
val groupId: String
}

View File

@ -0,0 +1,11 @@
package im.vector.matrix.android.api.session.group

import android.arch.lifecycle.LiveData
import im.vector.matrix.android.api.session.group.model.GroupSummary

interface GroupService {

fun getGroup(groupId: String): Group?

fun liveGroupSummaries(): LiveData<List<GroupSummary>>
}

View File

@ -0,0 +1,8 @@
package im.vector.matrix.android.api.session.group.model

data class GroupSummary(
val groupId: String,
val displayName: String = "",
val shortDescription: String = "",
val avatarUrl: String = ""
)

View File

@ -0,0 +1,19 @@
package im.vector.matrix.android.internal.database.mapper

import im.vector.matrix.android.api.session.group.Group
import im.vector.matrix.android.internal.database.model.GroupEntity
import im.vector.matrix.android.internal.session.group.DefaultGroup


object GroupMapper {

internal fun map(groupEntity: GroupEntity): Group {
return DefaultGroup(
groupEntity.groupId
)
}
}

fun GroupEntity.asDomain(): Group {
return GroupMapper.map(this)
}

View File

@ -0,0 +1,21 @@
package im.vector.matrix.android.internal.database.mapper

import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity


object GroupSummaryMapper {

internal fun map(roomSummaryEntity: GroupSummaryEntity): GroupSummary {
return GroupSummary(
roomSummaryEntity.groupId,
roomSummaryEntity.displayName,
roomSummaryEntity.shortDescription,
roomSummaryEntity.avatarUrl
)
}
}

fun GroupSummaryEntity.asDomain(): GroupSummary {
return GroupSummaryMapper.map(this)
}

View File

@ -0,0 +1,22 @@
package im.vector.matrix.android.internal.database.model

import im.vector.matrix.android.api.session.room.model.MyMembership
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import kotlin.properties.Delegates

open class GroupEntity(@PrimaryKey var groupId: String = ""

) : RealmObject() {

private var membershipStr: String = MyMembership.NONE.name

@delegate:Ignore
var membership: MyMembership by Delegates.observable(MyMembership.valueOf(membershipStr)) { _, _, newValue ->
membershipStr = newValue.name
}

companion object

}

View File

@ -0,0 +1,14 @@
package im.vector.matrix.android.internal.database.model

import io.realm.RealmObject
import io.realm.annotations.PrimaryKey

open class GroupSummaryEntity(@PrimaryKey var groupId: String = "",
var displayName: String = "",
var shortDescription: String = "",
var avatarUrl: String = ""
) : RealmObject() {

companion object

}

View File

@ -0,0 +1,20 @@
package im.vector.matrix.android.internal.database.query

import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.internal.database.model.GroupEntity
import im.vector.matrix.android.internal.database.model.GroupEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where

fun GroupEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<GroupEntity> {
return realm.where<GroupEntity>().equalTo(GroupEntityFields.GROUP_ID, roomId)
}

fun GroupEntity.Companion.where(realm: Realm, membership: MyMembership? = null): RealmQuery<GroupEntity> {
val query = realm.where<GroupEntity>()
if (membership != null) {
query.equalTo(GroupEntityFields.MEMBERSHIP_STR, membership.name)
}
return query
}

View File

@ -0,0 +1,16 @@
package im.vector.matrix.android.internal.database.query

import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where

fun GroupSummaryEntity.Companion.where(realm: Realm, groupId: String? = null): RealmQuery<GroupSummaryEntity> {
val query = realm.where<GroupSummaryEntity>()
if (groupId != null) {
query.equalTo(GroupSummaryEntityFields.GROUP_ID, groupId)
}
return query
}

View File

@ -16,7 +16,6 @@
*/
package im.vector.matrix.android.internal.legacy.rest.model.group;

import java.io.Serializable;
import java.util.Map;

/**

View File

@ -4,10 +4,15 @@ import android.arch.lifecycle.LiveData
import android.os.Looper
import android.support.annotation.MainThread
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.Group
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.internal.auth.data.SessionParams
import im.vector.matrix.android.internal.session.group.GroupModule
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.room.RoomModule
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.sync.SyncModule
@ -28,7 +33,9 @@ class DefaultSession(override val sessionParams: SessionParams) : Session, KoinC
private lateinit var scope: Scope

private val roomSummaryObserver by inject<RoomSummaryUpdater>()
private val groupSummaryUpdater by inject<GroupSummaryUpdater>()
private val roomService by inject<RoomService>()
private val groupService by inject<GroupService>()
private val syncThread by inject<SyncThread>()
private var isOpen = false

@ -40,9 +47,11 @@ class DefaultSession(override val sessionParams: SessionParams) : Session, KoinC
val sessionModule = SessionModule(sessionParams)
val syncModule = SyncModule()
val roomModule = RoomModule()
StandAloneContext.loadKoinModules(listOf(sessionModule, syncModule, roomModule))
val groupModule = GroupModule()
StandAloneContext.loadKoinModules(listOf(sessionModule, syncModule, roomModule, groupModule))
scope = getKoin().getOrCreateScope(SCOPE)
roomSummaryObserver.start()
groupSummaryUpdater.start()
syncThread.start()
}

@ -52,6 +61,7 @@ class DefaultSession(override val sessionParams: SessionParams) : Session, KoinC
checkIsMainThread()
assert(isOpen)
syncThread.kill()
groupSummaryUpdater.dispose()
roomSummaryObserver.dispose()
scope.close()
isOpen = false
@ -89,6 +99,18 @@ class DefaultSession(override val sessionParams: SessionParams) : Session, KoinC
roomService.saveLastSelectedRoom(roomSummary)
}

// GROUP SERVICE

override fun getGroup(groupId: String): Group? {
assert(isOpen)
return groupService.getGroup(groupId)
}

override fun liveGroupSummaries(): LiveData<List<GroupSummary>> {
assert(isOpen)
return groupService.liveGroupSummaries()
}

// Private methods *****************************************************************************

private fun checkIsMainThread() {

View File

@ -1,8 +1,11 @@
package im.vector.matrix.android.internal.session

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.internal.auth.data.SessionParams
import im.vector.matrix.android.internal.session.group.DefaultGroupService
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.room.DefaultRoomService
import im.vector.matrix.android.internal.session.room.RoomAvatarResolver
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
@ -58,6 +61,14 @@ class SessionModule(private val sessionParams: SessionParams) : Module {
DefaultRoomService(get()) as RoomService
}

scope(DefaultSession.SCOPE) {
GroupSummaryUpdater(get(), get())
}

scope(DefaultSession.SCOPE) {
DefaultGroupService(get()) as GroupService
}

}.invoke()



View File

@ -0,0 +1,7 @@
package im.vector.matrix.android.internal.session.group

import im.vector.matrix.android.api.session.group.Group

class DefaultGroup(override val groupId: String) : Group {

}

View File

@ -0,0 +1,26 @@
package im.vector.matrix.android.internal.session.group

import android.arch.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.group.Group
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where

class DefaultGroupService(private val monarchy: Monarchy) : GroupService {

override fun getGroup(groupId: String): Group? {
return null
}

override fun liveGroupSummaries(): LiveData<List<GroupSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) },
{ it.asDomain() }
)
}

}

View File

@ -0,0 +1,68 @@
package im.vector.matrix.android.internal.session.group

import arrow.core.Either
import arrow.core.flatMap
import arrow.core.leftIfNull
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse
import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import io.realm.kotlin.createObject
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class GetGroupSummaryRequest(
private val groupAPI: GroupAPI,
private val monarchy: Monarchy,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) {

fun execute(groupId: String,
callback: MatrixCallback<GroupSummaryResponse>
): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val groupOrFailure = execute(groupId)
groupOrFailure.bimap({ callback.onFailure(it) }, { callback.onSuccess(it) })
}
return CancelableCoroutine(job)
}

private suspend fun execute(groupId: String) = withContext(coroutineDispatchers.io) {

return@withContext executeRequest<GroupSummaryResponse> {
apiCall = groupAPI.getSummary(groupId)
}.leftIfNull {
Failure.Unknown(RuntimeException("GroupSummary shouldn't be null"))
}.flatMap { groupSummary ->
try {
insertInDb(groupSummary, groupId)
Either.right(groupSummary)
} catch (exception: Exception) {
Either.Left(Failure.Unknown(exception))
}
}
}

private fun insertInDb(groupSummary: GroupSummaryResponse, groupId: String) {
monarchy.runTransactionSync { realm ->
val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst()
?: realm.createObject(groupId)

groupSummaryEntity.avatarUrl = groupSummary.profile?.avatarUrl ?: ""
val name = groupSummary.profile?.name
groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name
groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: ""


}
}


}

View File

@ -0,0 +1,21 @@
package im.vector.matrix.android.internal.session.group

import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse
import kotlinx.coroutines.Deferred
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path

interface GroupAPI {

/**
* Request a group summary
*
* @param groupId the group id
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/summary")
fun getSummary(@Path("groupId") groupId: String): Deferred<Response<GroupSummaryResponse>>


}

View File

@ -0,0 +1,23 @@
package im.vector.matrix.android.internal.session.group

import im.vector.matrix.android.internal.session.DefaultSession
import org.koin.dsl.context.ModuleDefinition
import org.koin.dsl.module.Module
import org.koin.dsl.module.module
import retrofit2.Retrofit

class GroupModule : Module {

override fun invoke(): ModuleDefinition = module(override = true) {

scope(DefaultSession.SCOPE) {
val retrofit: Retrofit = get()
retrofit.create(GroupAPI::class.java)
}

scope(DefaultSession.SCOPE) {
GetGroupSummaryRequest(get(), get(), get())
}

}.invoke()
}

View File

@ -0,0 +1,63 @@
package im.vector.matrix.android.internal.session.group

import android.arch.lifecycle.Observer
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.group.Group
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.GroupEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean

internal class GroupSummaryUpdater(private val monarchy: Monarchy,
private val getGroupSummaryRequest: GetGroupSummaryRequest
) : Observer<Monarchy.ManagedChangeSet<GroupEntity>> {

private var isStarted = AtomicBoolean(false)
private val liveResults = monarchy.findAllManagedWithChanges { GroupEntity.where(it) }

fun start() {
if (isStarted.compareAndSet(false, true)) {
liveResults.observeForever(this)
}
}

fun dispose() {
if (isStarted.compareAndSet(true, false)) {
liveResults.removeObserver(this)
}
}

// PRIVATE

override fun onChanged(changeSet: Monarchy.ManagedChangeSet<GroupEntity>?) {
if (changeSet == null) {
return
}
val groups = changeSet.realmResults.map { it.asDomain() }
val indexesToUpdate = changeSet.orderedCollectionChangeSet.changes + changeSet.orderedCollectionChangeSet.insertions
updateGroupList(groups, indexesToUpdate)
}


private fun updateGroupList(groups: List<Group>, indexes: IntArray) {
indexes.forEach {
val group = groups[it]
try {
updateGroup(group)
} catch (e: Exception) {
Timber.e(e, "An error occured when updating room summaries")
}
}
}

private fun updateGroup(group: Group?) {
if (group == null) {
return
}
getGroupSummaryRequest.execute(group.groupId, object : MatrixCallback<GroupSummaryResponse> {})
}

}

View File

@ -0,0 +1,33 @@
package im.vector.matrix.android.internal.session.group.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* This class represents a community profile in the server responses.
*/
@JsonClass(generateAdapter = true)
data class GroupProfile(

@Json(name = "short_description") val shortDescription: String? = null,

/**
* Tell whether the group is public.
*/
@Json(name = "is_public") val isPublic: Boolean? = null,

/**
* The URL for the group's avatar. May be nil.
*/
@Json(name = "avatar_url") val avatarUrl: String? = null,

/**
* The group's name.
*/
@Json(name = "name") val name: String? = null,

/**
* The optional HTML formatted string used to described the group.
*/
@Json(name = "long_description") val longDescription: String? = null
)

View File

@ -0,0 +1,30 @@
package im.vector.matrix.android.internal.session.group.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* This class represents the summary of a community in the server response.
*/
@JsonClass(generateAdapter = true)
data class GroupSummaryResponse(
/**
* The group profile.
*/
@Json(name = "profile") val profile: GroupProfile? = null,

/**
* The group users.
*/
@Json(name = "users_section") val usersSection: GroupSummaryUsersSection? = null,

/**
* The current user status.
*/
@Json(name = "user") val user: GroupSummaryUser? = null,

/**
* The rooms linked to the community.
*/
@Json(name = "rooms_section") val roomsSection: GroupSummaryRoomsSection? = null
)

View File

@ -0,0 +1,16 @@
package im.vector.matrix.android.internal.session.group.model

import com.squareup.moshi.Json

/**
* This class represents the community rooms in a group summary response.
*/
data class GroupSummaryRoomsSection(

@Json(name = "total_room_count_estimate") val totalRoomCountEstimate: Int? = null,

@Json(name = "rooms") val rooms: List<String> = emptyList()

// @TODO: Check the meaning and the usage of these categories. This dictionary is empty FTM.
//public Map<Object, Object> categories;
)

View File

@ -0,0 +1,21 @@
package im.vector.matrix.android.internal.session.group.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* This class represents the current user status in a group summary response.
*/
@JsonClass(generateAdapter = true)
data class GroupSummaryUser(

/**
* The current user membership in this community.
*/
@Json(name = "membership") val membership: String? = null,

/**
* Tell whether the user published this community on his profile.
*/
@Json(name = "is_publicised") val isPublicised: Boolean? = null
)

View File

@ -0,0 +1,20 @@
package im.vector.matrix.android.internal.session.group.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass


/**
* This class represents the community members in a group summary response.
*/

@JsonClass(generateAdapter = true)
data class GroupSummaryUsersSection(

@Json(name = "total_user_count_estimate") val totalUserCountEstimate: Int,

@Json(name = "users") val users: List<String> = emptyList()

// @TODO: Check the meaning and the usage of these roles. This dictionary is empty FTM.
//public Map<Object, Object> roles;
)

View File

@ -55,7 +55,7 @@ class DefaultTimelineHolder(private val roomId: String,
.setEnablePlaceholders(false)
.setPageSize(PAGE_SIZE)
.setInitialLoadSizeHint(PAGE_SIZE)
.setPrefetchDistance(10)
.setPrefetchDistance(20)
.build()

val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig).setBoundaryCallback(boundaryCallback)

View File

@ -0,0 +1,66 @@
package im.vector.matrix.android.internal.session.sync

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.internal.database.model.GroupEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.legacy.rest.model.group.GroupsSyncResponse
import im.vector.matrix.android.internal.legacy.rest.model.group.InvitedGroupSync
import io.realm.Realm


internal class GroupSyncHandler(private val monarchy: Monarchy) {

sealed class HandlingStrategy {
data class JOINED(val data: Map<String, Any>) : HandlingStrategy()
data class INVITED(val data: Map<String, InvitedGroupSync>) : HandlingStrategy()
data class LEFT(val data: Map<String, Any>) : HandlingStrategy()
}

fun handle(roomsSyncResponse: GroupsSyncResponse) {
monarchy.runTransactionSync { realm ->
handleGroupSync(realm, GroupSyncHandler.HandlingStrategy.JOINED(roomsSyncResponse.join))
handleGroupSync(realm, GroupSyncHandler.HandlingStrategy.INVITED(roomsSyncResponse.invite))
handleGroupSync(realm, GroupSyncHandler.HandlingStrategy.LEFT(roomsSyncResponse.leave))
}
}

// PRIVATE METHODS *****************************************************************************

private fun handleGroupSync(realm: Realm, handlingStrategy: HandlingStrategy) {
val groups = when (handlingStrategy) {
is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedGroup(realm, it.key) }
is HandlingStrategy.INVITED -> handlingStrategy.data.map { handleInvitedGroup(realm, it.key) }
is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftGroup(realm, it.key) }
}
realm.insertOrUpdate(groups)
}

private fun handleJoinedGroup(realm: Realm,
groupId: String): GroupEntity {

val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId)
groupEntity.membership = MyMembership.JOINED
return groupEntity
}

private fun handleInvitedGroup(realm: Realm,
groupId: String): GroupEntity {

val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId)
groupEntity.membership = MyMembership.INVITED
return groupEntity

}

// TODO : handle it
private fun handleLeftGroup(realm: Realm,
groupId: String): GroupEntity {

val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId)
groupEntity.membership = MyMembership.LEFT
return groupEntity
}


}

View File

@ -29,12 +29,16 @@ class SyncModule : Module {
RoomSyncHandler(get(), get(), get())
}

scope(DefaultSession.SCOPE) {
GroupSyncHandler(get())
}

scope(DefaultSession.SCOPE) {
UserAccountDataSyncHandler(get())
}

scope(DefaultSession.SCOPE) {
SyncResponseHandler(get(), get())
SyncResponseHandler(get(), get(), get())
}

scope(DefaultSession.SCOPE) {

View File

@ -4,7 +4,8 @@ import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import timber.log.Timber

internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler,
private val userAccountDataSyncHandler: UserAccountDataSyncHandler) {
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
private val groupSyncHandler: GroupSyncHandler) {

fun handleResponse(syncResponse: SyncResponse?, fromToken: String?, isCatchingUp: Boolean) {
if (syncResponse == null) {
@ -14,6 +15,9 @@ internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler,
if (syncResponse.rooms != null) {
roomSyncHandler.handle(syncResponse.rooms)
}
if (syncResponse.groups != null) {
groupSyncHandler.handle(syncResponse.groups)
}
if (syncResponse.accountData != null) {
userAccountDataSyncHandler.handle(syncResponse.accountData)
}