Creates a Widget Manager to be used internally and state event service

This commit is contained in:
ganfra 2020-05-06 20:49:07 +02:00
parent 4fdd2f4eed
commit b047f36e86
16 changed files with 375 additions and 33 deletions

View File

@ -16,6 +16,7 @@
package im.vector.matrix.rx
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
@ -60,7 +61,7 @@ class RxRoom(private val room: Room) {
}
}
fun liveStateEvent(eventType: String, stateKey: String): Observable<Optional<Event>> {
fun liveStateEvent(eventType: String, stateKey: QueryStringValue): Observable<Optional<Event>> {
return room.getStateEventLive(eventType, stateKey).asObservable()
.startWithCallable {
room.getStateEvent(eventType, stateKey).toOptional()

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.state
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Optional
@ -28,7 +29,11 @@ interface StateService {
*/
fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
fun getStateEvent(eventType: String, stateKey: String): Event?
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
fun getStateEventLive(eventType: String, stateKey: String): LiveData<Optional<Event>>
fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<Optional<Event>>
fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): List<Event>
fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<List<Event>>
}

View File

@ -23,7 +23,7 @@ import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.createObject
internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: String, type: String): RealmQuery<CurrentStateEventEntity> {
internal fun CurrentStateEventEntity.Companion.whereType(realm: Realm, roomId: String, type: String): RealmQuery<CurrentStateEventEntity> {
return realm.where(CurrentStateEventEntity::class.java)
.equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
.equalTo(CurrentStateEventEntityFields.TYPE, type)
@ -31,7 +31,7 @@ internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: Strin
internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String)
: RealmQuery<CurrentStateEventEntity> {
return where(realm = realm, roomId = roomId, type = type)
return whereType(realm = realm, roomId = roomId, type = type)
.equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey)
}

View File

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.pushrules.ContainsDisplayNameCondition
import im.vector.matrix.android.api.pushrules.EventMatchCondition
import im.vector.matrix.android.api.pushrules.RoomMemberCountCondition
import im.vector.matrix.android.api.pushrules.SenderNotificationPermissionCondition
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
@ -48,7 +49,7 @@ internal class DefaultConditionResolver @Inject constructor(
val roomId = event.roomId ?: return false
val room = roomGetter.getRoom(roomId) ?: return false
val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "")
val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
?.content
?.toModel<PowerLevelsContent>()
?: PowerLevelsContent()

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
@ -175,7 +176,7 @@ internal interface RoomAPI {
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}")
fun sendStateEvent(@Path("roomId") roomId: String,
@Path("state_event_type") stateEventType: String,
@Body params: Map<String, String>): Call<Unit>
@Body params: JsonDict): Call<Unit>
/**
* Send a generic state events
@ -189,7 +190,7 @@ internal interface RoomAPI {
fun sendStateEvent(@Path("roomId") roomId: String,
@Path("state_event_type") stateEventType: String,
@Path("state_key") stateKey: String,
@Body params: Map<String, String>): Call<Unit>
@Body params: JsonDict): Call<Unit>
/**
* Send a relation event to a room.

View File

@ -17,26 +17,19 @@
package im.vector.matrix.android.internal.session.room.state
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.query.getOrNull
import im.vector.matrix.android.internal.database.query.whereStateKey
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.Realm
internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy,
private val stateEventDataSource: StateEventDataSource,
private val taskExecutor: TaskExecutor,
private val sendStateTask: SendStateTask
) : StateService {
@ -46,20 +39,20 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
fun create(roomId: String): StateService
}
override fun getStateEvent(eventType: String, stateKey: String): Event? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
CurrentStateEventEntity.getOrNull(realm, roomId, type = eventType, stateKey = stateKey)?.root?.asDomain()
}
override fun getStateEvent(eventType: String, stateKey: QueryStringValue): Event? {
return stateEventDataSource.getStateEvent(roomId, eventType, stateKey)
}
override fun getStateEventLive(eventType: String, stateKey: String): LiveData<Optional<Event>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm -> CurrentStateEventEntity.whereStateKey(realm, roomId, type = eventType, stateKey = "") },
{ it.root?.asDomain() }
)
return Transformations.map(liveData) { results ->
results.firstOrNull().toOptional()
}
override fun getStateEventLive(eventType: String, stateKey: QueryStringValue): LiveData<Optional<Event>> {
return stateEventDataSource.getStateEventLive(roomId, eventType, stateKey)
}
override fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> {
return stateEventDataSource.getStateEvents(roomId, eventTypes, stateKey)
}
override fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue): LiveData<List<Event>> {
return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey)
}
override fun updateTopic(topic: String, callback: MatrixCallback<Unit>) {

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.session.room.state
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
@ -26,7 +27,7 @@ internal interface SendStateTask : Task<SendStateTask.Params, Unit> {
data class Params(
val roomId: String,
val eventType: String,
val body: Map<String, String>
val body: JsonDict
)
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.state
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields
import im.vector.matrix.android.internal.query.process
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
import javax.inject.Inject
internal class StateEventDataSource @Inject constructor(private val monarchy: Monarchy) {
fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue): Event? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
buildStateEventQuery(realm, roomId, setOf(eventType), stateKey).findFirst()?.root?.asDomain()
}
}
fun getStateEventLive(roomId: String, eventType: String, stateKey: QueryStringValue): LiveData<Optional<Event>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm -> buildStateEventQuery(realm, roomId, setOf(eventType), stateKey) },
{ it.root?.asDomain() }
)
return Transformations.map(liveData) { results ->
results.firstOrNull().toOptional()
}
}
fun getStateEvents(roomId: String, eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
buildStateEventQuery(realm, roomId, eventTypes, stateKey)
.findAll()
.mapNotNull {
it.root?.asDomain()
}
}
}
fun getStateEventsLive(roomId: String, eventTypes: Set<String>, stateKey: QueryStringValue): LiveData<List<Event>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm -> buildStateEventQuery(realm, roomId, eventTypes, stateKey) },
{ it.root?.asDomain() }
)
return Transformations.map(liveData) { results ->
results.filterNotNull()
}
}
private fun buildStateEventQuery(realm: Realm,
roomId: String,
eventTypes: Set<String>,
stateKey: QueryStringValue
): RealmQuery<CurrentStateEventEntity> {
return realm.where<CurrentStateEventEntity>()
.equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
.`in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray())
.process(CurrentStateEventEntityFields.STATE_KEY, stateKey)
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.widgets
import im.vector.matrix.android.api.failure.Failure
sealed class CreateWidgetFailure : Failure.FeatureFailure() {
object NotEnoughtPower : CreateWidgetFailure()
object CreationFailed : CreateWidgetFailure()
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.widgets
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.internal.database.awaitNotEmptyResult
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields
import im.vector.matrix.android.internal.database.query.whereStateKey
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface CreateWidgetTask : Task<CreateWidgetTask.Params, Unit> {
data class Params(
val roomId: String,
val widgetId: String,
val content: Content
)
}
internal class DefaultCreateWidgetTask @Inject constructor(private val monarchy: Monarchy,
private val roomAPI: RoomAPI,
@UserId private val userId: String,
private val eventBus: EventBus) : CreateWidgetTask {
override suspend fun execute(params: CreateWidgetTask.Params) {
executeRequest<Unit>(eventBus) {
apiCall = roomAPI.sendStateEvent(
roomId = params.roomId,
stateEventType = WidgetManager.WIDGET_EVENT_TYPE,
stateKey = params.widgetId,
params = params.content
)
}
awaitNotEmptyResult(monarchy.realmConfiguration, 30_000L) {
CurrentStateEventEntity
.whereStateKey(it, params.roomId, type = WidgetManager.WIDGET_EVENT_TYPE, stateKey = params.widgetId)
.and()
.equalTo(CurrentStateEventEntityFields.ROOT.SENDER, userId)
}
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.widgets
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.widgets.model.WidgetContent
data class Widget(
private val widgetContent: WidgetContent,
private val event: Event
)

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.widgets
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.android.api.session.widgets.model.WidgetContent
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager
import im.vector.matrix.android.internal.session.room.state.StateEventDataSource
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.launchToCallback
import java.util.HashMap
import javax.inject.Inject
@SessionScope
internal class WidgetManager @Inject constructor(private val integrationManager: IntegrationManager,
private val stateEventDataSource: StateEventDataSource,
private val taskExecutor: TaskExecutor,
private val createWidgetTask: CreateWidgetTask,
@UserId private val userId: String) : IntegrationManager.Listener {
companion object {
const val WIDGET_EVENT_TYPE = "im.vector.modular.widgets"
}
private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry }
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner)
fun start() {
lifecycleRegistry.currentState = Lifecycle.State.STARTED
integrationManager.addListener(this)
}
fun stop() {
integrationManager.removeListener(this)
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
}
fun getRoomWidgets(
roomId: String,
widgetId: QueryStringValue = QueryStringValue.NoCondition,
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): List<Widget> {
// Get all im.vector.modular.widgets state events in the room
val widgetEvents: List<Event> = stateEventDataSource.getStateEvents(roomId, setOf(WIDGET_EVENT_TYPE), widgetId)
// Widget id -> widget
val widgets: MutableMap<String, Widget> = HashMap()
// Order widgetEvents with the last event first
// There can be several im.vector.modular.widgets state events for a same widget but
// only the last one must be considered.
val sortedWidgetEvents = widgetEvents.sortedByDescending {
it.originServerTs
}
// Create each widget from its latest im.vector.modular.widgets state event
for (widgetEvent in sortedWidgetEvents) { // Filter widget types if required
val widgetContent = widgetEvent.content.toModel<WidgetContent>()
if (widgetContent?.url == null) continue
val widgetType = widgetContent.type ?: continue
if (widgetTypes != null && !widgetTypes.contains(widgetType)) {
continue
}
if (excludedTypes != null && excludedTypes.contains(widgetType)) {
continue
}
// widgetEvent.stateKey = widget id
if (widgetEvent.stateKey != null && !widgets.containsKey(widgetEvent.stateKey)) {
val widget = Widget(widgetContent, widgetEvent)
widgets[widgetEvent.stateKey] = widget
}
}
return widgets.values.toList()
}
fun createWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback<Widget>): Cancelable {
return taskExecutor.executorScope.launchToCallback(callback = callback) {
if (!hasPermissionsToHandleWidgets(roomId)) {
throw CreateWidgetFailure.NotEnoughtPower
}
val params = CreateWidgetTask.Params(
roomId = roomId,
widgetId = widgetId,
content = content
)
createWidgetTask.execute(params)
try {
getRoomWidgets(roomId, widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.INSENSITIVE)).first()
} catch (failure: Throwable) {
throw CreateWidgetFailure.CreationFailed
}
}
}
fun hasPermissionsToHandleWidgets(roomId: String): Boolean {
val powerLevelsEvent = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false
return PowerLevelsHelper(powerLevelsContent).isAllowedToSend(EventType.STATE_ROOM_POWER_LEVELS, userId)
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.widgets.token
internal class ScalarTokenStore {
}

View File

@ -1002,7 +1002,7 @@ class RoomDetailViewModel @AssistedInject constructor(
setState { copy(asyncInviter = Success(it)) }
}
}
room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, "")?.also {
room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE)?.also {
setState { copy(tombstoneEvent = it) }
}
}

View File

@ -184,7 +184,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
private fun observeRoomSummaryAndPowerLevels(room: Room) {
val roomSummaryLive = room.rx().liveRoomSummary().unwrap()
val powerLevelsContentLive = room.rx().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "")
val powerLevelsContentLive = room.rx().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap()

View File

@ -80,7 +80,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
.combineLatest<List<RoomMemberSummary>, PowerLevelsContent, RoomMemberSummaries>(
room.rx().liveRoomMembers(roomMemberQueryParams),
room.rx()
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "")
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap(),
BiFunction { roomMembers, powerLevelsContent ->