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