Start creating integration manager

This commit is contained in:
ganfra 2020-05-06 11:53:35 +02:00
parent 29c9d3070c
commit 182753e4ec
7 changed files with 340 additions and 9 deletions

View File

@ -22,11 +22,11 @@ import im.vector.matrix.android.api.util.JsonDict
@JsonClass(generateAdapter = true)
data class WidgetContent(
@Json(name = "creatorUserId") val creatorUserId: String,
@Json(name = "id") val id: String,
@Json(name = "type") val type: String,
@Json(name = "url") val url: String,
@Json(name = "name") val name: String,
@Json(name = "data") val data: JsonDict,
@Json(name = "creatorUserId") val creatorUserId: String? = null,
@Json(name = "id") val id: String? = null,
@Json(name = "type") val type: String? = null,
@Json(name = "url") val url: String? = null,
@Json(name = "name") val name: String? = null,
@Json(name = "data") val data: JsonDict = emptyMap(),
@Json(name = "waitForIframeLoad") val waitForIframeLoad: Boolean = false
)

View File

@ -0,0 +1,29 @@
/*
* 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.extensions
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
this.observe(owner, Observer { observer(it) })
}
inline fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
this.observe(owner, Observer { it?.run(observer) })
}

View File

@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.job.SyncThread
@ -97,6 +98,7 @@ internal class DefaultSession @Inject constructor(
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val accountService: Lazy<AccountService>,
private val timelineEventDecryptor: TimelineEventDecryptor,
private val integrationManager: IntegrationManager,
private val shieldTrustUpdater: ShieldTrustUpdater)
: Session,
RoomService by roomService.get(),
@ -133,6 +135,7 @@ internal class DefaultSession @Inject constructor(
eventBus.register(this)
timelineEventDecryptor.start()
shieldTrustUpdater.start()
integrationManager.start()
}
override fun requireBackgroundSync() {
@ -175,6 +178,7 @@ internal class DefaultSession @Inject constructor(
isOpen = false
eventBus.unregister(this)
shieldTrustUpdater.stop()
integrationManager.stop()
}
override fun getSyncStateLive(): LiveData<SyncState> {

View File

@ -24,7 +24,7 @@ data class AllowedWidgetsContent(
/**
* Map of stateEventId to Allowed
*/
@Json(name = "widgets") val widgets: Map<String, Boolean>,
@Json(name = "widgets") val widgets: Map<String, Boolean> = emptyMap(),
/**
* Map of native widgetType to a map of domain to Allowed
@ -35,5 +35,5 @@ data class AllowedWidgetsContent(
* }
* }
*/
@Json(name = "native_widgets") val native: Map<String, Map<String, Boolean>>
@Json(name = "native_widgets") val native: Map<String, Map<String, Boolean>> = emptyMap()
)

View File

@ -0,0 +1,288 @@
/*
* 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.integrationmanager
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.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.widgets.model.WidgetContent
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.extensions.observeNotNull
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import timber.log.Timber
import javax.inject.Inject
/**
* The integration manager allows to
* - Get the Integration Manager that a user has explicitly set for its account (via account data)
* - Get the recommended/preferred Integration Manager list as defined by the HomeServer (via wellknown)
* - Check if the user has disabled the integration manager feature
* - Allow / Disallow Integration manager (propagated to other riot clients)
*
* The integration manager listen to account data, and can notify observer for changes.
*
* The wellknown is refreshed at each application fresh start
*
*/
@SessionScope
internal class IntegrationManager @Inject constructor(private val taskExecutor: TaskExecutor,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val accountDataDataSource: AccountDataDataSource) {
interface Listener {
fun onIsEnabledChanged(enabled: Boolean) {
//No-op
}
fun onConfigurationChanged(config: IntegrationManagerConfig) {
//No-op
}
fun onWidgetPermissionsChanged(widgets: Map<String, Boolean>) {
//No-op
}
}
private var currentConfig: IntegrationManagerConfig? = null
private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry }
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner)
private val listeners = HashSet<Listener>()
fun addListener(listener: Listener) = synchronized(listeners) { listeners.add(listener) }
fun removeListener(listener: Listener) = synchronized(listeners) { listeners.remove(listener) }
fun start() {
lifecycleRegistry.currentState = Lifecycle.State.STARTED
accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS)
.observeNotNull(lifecycleOwner) {
val allowedWidgetsContent = it.getOrNull()?.content?.toModel<AllowedWidgetsContent>()
if (allowedWidgetsContent != null) {
notifyWidgetPermissionsChanged(allowedWidgetsContent)
}
}
accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING)
.observeNotNull(lifecycleOwner) {
val integrationProvisioningContent = it.getOrNull()?.content?.toModel<IntegrationProvisioningContent>()
if (integrationProvisioningContent != null) {
notifyIsEnabledChanged(integrationProvisioningContent)
}
}
accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.TYPE_WIDGETS)
.observeNotNull(lifecycleOwner) {
val integrationManager = it.getOrNull()?.asIntegrationManagerWidgetContent()
val config = integrationManager?.extractIntegrationManagerConfig()
if (config != null && config != currentConfig) {
currentConfig = config
notifyConfigurationChanged(config)
}
}
}
fun stop() {
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
}
/**
* Returns false if the user as disabled integration manager feature
*/
fun isIntegrationEnabled(): Boolean {
val integrationProvisioningData = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING)
val integrationProvisioningContent = integrationProvisioningData?.content?.toModel<IntegrationProvisioningContent>()
return integrationProvisioningContent?.enabled ?: false
}
fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback<Unit>): Cancelable {
val isIntegrationEnabled = isIntegrationEnabled()
if (enable == isIntegrationEnabled) {
callback.onSuccess(Unit)
return NoOpCancellable
}
val integrationProvisioningContent = IntegrationProvisioningContent(enabled = enable)
val params = UpdateUserAccountDataTask.IntegrationProvisioning(integrationProvisioningContent = integrationProvisioningContent)
return updateUserAccountDataTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
val newContent = if (currentContent == null) {
val allowedWidget = mapOf(stateEventId to allowed)
AllowedWidgetsContent(widgets = allowedWidget, native = emptyMap())
} else {
val allowedWidgets = currentContent.widgets.toMutableMap().apply {
put(stateEventId, allowed)
}
currentContent.copy(widgets = allowedWidgets)
}
val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent)
return updateUserAccountDataTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
fun isWidgetAllowed(stateEventId: String): Boolean {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
return currentContent?.widgets?.get(stateEventId) ?: false
}
fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
val newContent = if (currentContent == null) {
val nativeAllowedWidgets = mapOf(widgetType to mapOf(domain to allowed))
AllowedWidgetsContent(widgets = emptyMap(), native = nativeAllowedWidgets)
} else {
val nativeAllowedWidgets = currentContent.native.toMutableMap().apply {
(get(widgetType))?.let {
set(widgetType, it.toMutableMap().apply { set(domain, allowed) })
} ?: run {
set(widgetType, mapOf(domain to allowed))
}
}
currentContent.copy(native = nativeAllowedWidgets)
}
val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent)
return updateUserAccountDataTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
fun isNativeWidgetAllowed(widgetType: String, domain: String?): Boolean {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
return currentContent?.native?.get(widgetType)?.get(domain) ?: false
}
private fun notifyConfigurationChanged(config: IntegrationManagerConfig) {
Timber.v("On configuration changed : $config")
synchronized(listeners) {
listeners.forEach {
try {
it.onConfigurationChanged(config)
} catch (t: Throwable) {
Timber.e(t, "Failed to notify listener")
}
}
}
}
private fun notifyWidgetPermissionsChanged(allowedWidgets: AllowedWidgetsContent) {
Timber.v("On widget permissions changed: $allowedWidgets")
synchronized(listeners) {
listeners.forEach {
try {
it.onWidgetPermissionsChanged(allowedWidgets.widgets)
} catch (t: Throwable) {
Timber.e(t, "Failed to notify listener")
}
}
}
}
private fun notifyIsEnabledChanged(provisioningContent: IntegrationProvisioningContent) {
Timber.v("On provisioningContent changed : $provisioningContent")
synchronized(listeners) {
listeners.forEach {
try {
it.onIsEnabledChanged(provisioningContent.enabled)
} catch (t: Throwable) {
Timber.e(t, "Failed to notify listener")
}
}
}
}
/*
private fun getStoreWellknownIM(): List<IntegrationManagerConfig> {
val prefs = context.getSharedPreferences(PREFS_IM, Context.MODE_PRIVATE)
return prefs.getString(WELLKNOWN_KEY, null)?.let {
try {
Gson().fromJson<List<WellKnownManagerConfig>>(it,
object : TypeToken<List<WellKnownManagerConfig>>() {}.type)
} catch (any: Throwable) {
emptyList<WellKnownManagerConfig>()
}
} ?: emptyList<WellKnownManagerConfig>()
}
private fun setStoreWellknownIM(list: List<WellKnownManagerConfig>) {
val prefs = context.getSharedPreferences(PREFS_IM, Context.MODE_PRIVATE)
try {
val serialized = Gson().toJson(list)
prefs.edit().putString(WELLKNOWN_KEY, serialized).apply()
} catch (any: Throwable) {
//nop
}
}
*/
fun getConfig(): IntegrationManagerConfig? {
val accountWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_WIDGETS) ?: return null
return accountWidgets.asIntegrationManagerWidgetContent()?.extractIntegrationManagerConfig()
}
private fun WidgetContent.extractIntegrationManagerConfig(): IntegrationManagerConfig? {
if (url.isNullOrBlank()) {
return null
}
val integrationManagerData = data.toModel<IntegrationManagerWidgetData>()
return IntegrationManagerConfig(
uiUrl = url,
apiUrl = integrationManagerData?.apiUrl ?: url
)
}
private fun UserAccountDataEvent.asIntegrationManagerWidgetContent(): WidgetContent? {
return content.asSequence()
.mapNotNull {
@Suppress("UNCHECKED_CAST")
(it.value as? Content)?.toModel<WidgetContent>()
}.filter {
it.type == INTEGRATION_MANAGER_WIDGET
}
.firstOrNull()
}
companion object {
private const val INTEGRATION_MANAGER_WIDGET = "m.integration_manager"
private const val PREFS_IM = "IntegrationManager.Storage"
private const val WELLKNOWN_KEY = "WellKnown"
}
}

View File

@ -18,9 +18,10 @@ package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict
@JsonClass(generateAdapter = true)
data class UserAccountDataEvent(
@Json(name = "type") override val type: String,
@Json(name = "content") val content: Map<String, Any>
@Json(name = "content") val content: JsonDict
) : UserAccountData()

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.user.accountdata
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.integrationmanager.AllowedWidgetsContent
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationProvisioningContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.task.Task
@ -59,6 +60,14 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
}
}
data class IntegrationProvisioning(override val type: String = UserAccountData.ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING,
private val integrationProvisioningContent: IntegrationProvisioningContent) : Params {
override fun getData(): Any {
return integrationProvisioningContent
}
}
data class AnyParams(override val type: String,
private val any: Any
) : Params {