Widget: makes the integration manager screen mostly working

This commit is contained in:
ganfra 2020-05-26 08:07:09 +02:00
parent 00fd067c6b
commit df973a6275
34 changed files with 495 additions and 160 deletions

View file

@ -16,7 +16,9 @@
package im.vector.matrix.android.api.session.identity
sealed class IdentityServiceError : Throwable() {
import im.vector.matrix.android.api.failure.Failure
sealed class IdentityServiceError : Failure.FeatureFailure() {
object OutdatedIdentityServer : IdentityServiceError()
object OutdatedHomeServer : IdentityServiceError()
object NoIdentityServerConfigured : IdentityServiceError()

View file

@ -18,20 +18,27 @@ package im.vector.matrix.android.api.session.widgets
import android.webkit.WebView
import im.vector.matrix.android.api.util.JsonDict
import java.lang.reflect.Type
interface WidgetPostAPIMediator {
/**
* This initialize the mediator and configure the webview.
* This initialize the webview to handle.
* It will add a JavaScript Interface.
* Please call [clear] method when finished to clean the provided webview
* Please call [clearWebView] method when finished to clean the provided webview
*/
fun initialize(webView: WebView, handler: Handler)
fun setWebView(webView: WebView)
/**
* Set handler to communicate with the widgetPostAPIMediator.
* Please remove the reference by passing null when finished.
*/
fun setHandler(handler: Handler?)
/**
* This clear the mediator by removing the JavaScript Interface and cleaning references.
*/
fun clear()
fun clearWebView()
/**
* Inject the necessary javascript into the configured WebView.
@ -62,7 +69,7 @@ interface WidgetPostAPIMediator {
* @param response the response
* @param eventData the modular data
*/
fun <T> sendObjectResponse(klass: Class<T>, response: T?, eventData: JsonDict)
fun <T> sendObjectResponse(type: Type, response: T?, eventData: JsonDict)
/**
* Send success

View file

@ -25,7 +25,7 @@ import im.vector.matrix.android.internal.session.widgets.Widget
interface WidgetService {
fun getWidgetURLBuilder(): WidgetURLBuilder
fun getWidgetURLFormatter(): WidgetURLFormatter
fun getWidgetPostAPIMediator(): WidgetPostAPIMediator

View file

@ -16,10 +16,16 @@
package im.vector.matrix.android.api.session.widgets
interface WidgetURLBuilder {
interface WidgetURLFormatter {
/**
* Takes care of fetching a scalar token if required and build the final url.
* This methods can throw, you should take care of handling failure.
*/
suspend fun build(baseUrl: String, params: Map<String, String> = emptyMap(), forceFetchScalarToken: Boolean = false): String
suspend fun format(
baseUrl: String,
params: Map<String, String> = emptyMap(),
forceFetchScalarToken: Boolean = false,
bypassWhitelist: Boolean
): String
}

View file

@ -31,6 +31,5 @@ internal object NetworkConstants {
const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2"
const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/"
// TODO Ganfra, use correct value
const val URI_INTEGRATION_MANAGER_PATH = "TODO/"
const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/"
}

View file

@ -28,8 +28,14 @@ import javax.inject.Inject
class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
fun create(okHttpClient: Lazy<OkHttpClient>, baseUrl: String): Retrofit {
// ensure trailing /
val safeBaseUrl = if (!baseUrl.endsWith("/")) {
"$baseUrl/"
} else {
baseUrl
}
return Retrofit.Builder()
.baseUrl(baseUrl)
.baseUrl(safeBaseUrl)
.callFactory(object : Call.Factory {
override fun newCall(request: Request): Call {
return okHttpClient.get().newCall(request)

View file

@ -113,9 +113,10 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
callback.onFailure(InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported"))
}
else -> {
val params = SendStateTask.Params(roomId,
EventType.STATE_ROOM_ENCRYPTION,
mapOf(
val params = SendStateTask.Params(
roomId = roomId,
eventType = EventType.STATE_ROOM_ENCRYPTION,
body = mapOf(
"algorithm" to algorithm
))

View file

@ -79,7 +79,8 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
return sendStateEvent(
eventType = EventType.STATE_ROOM_TOPIC,
body = mapOf("topic" to topic),
callback = callback
callback = callback,
stateKey = null
)
}
}

View file

@ -23,7 +23,9 @@ import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.util.createUIHandler
import timber.log.Timber
import java.lang.reflect.Type
import java.util.HashMap
import javax.inject.Inject
@ -35,22 +37,28 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
private var handler: WidgetPostAPIMediator.Handler? = null
private var webView: WebView? = null
override fun initialize(webView: WebView, handler: WidgetPostAPIMediator.Handler) {
private val uiHandler = createUIHandler()
override fun setWebView(webView: WebView) {
this.webView = webView
this.handler = handler
webView.addJavascriptInterface(this, "WidgetPostAPIMediator")
webView.addJavascriptInterface(this, "Android")
}
override fun clear() {
handler = null
webView?.removeJavascriptInterface("WidgetPostAPIMediator")
override fun clearWebView() {
webView?.removeJavascriptInterface("Android")
webView = null
}
override fun setHandler(handler: WidgetPostAPIMediator.Handler?) {
this.handler = handler
}
override fun injectAPI() {
val js = widgetPostMessageAPIProvider.get()
if (null != js) {
webView?.loadUrl("javascript:$js")
if (js != null) {
uiHandler.post {
webView?.loadUrl("javascript:$js")
}
}
}
@ -111,10 +119,10 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
* @param response the response
* @param eventData the modular data
*/
override fun <T> sendObjectResponse(klass: Class<T>, response: T?, eventData: JsonDict) {
override fun <T> sendObjectResponse(type: Type, response: T?, eventData: JsonDict) {
var jsString: String? = null
if (response != null) {
val objectAdapter = moshi.adapter(klass)
val objectAdapter = moshi.adapter<T>(type)
try {
jsString = "JSON.parse('${objectAdapter.toJson(response)}')"
} catch (e: Exception) {
@ -157,7 +165,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
* @param jsString the response data
* @param eventData the modular data
*/
private fun sendResponse(jsString: String, eventData: JsonDict) {
private fun sendResponse(jsString: String, eventData: JsonDict) = uiHandler.post {
try {
val functionLine = "sendResponseFromRiotAndroid('" + eventData["_id"] + "' , " + jsString + ");"
Timber.v("BRIDGE sendResponse: $functionLine")

View file

@ -22,21 +22,21 @@ 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.widgets.WidgetPostAPIMediator
import im.vector.matrix.android.api.session.widgets.WidgetService
import im.vector.matrix.android.api.session.widgets.WidgetURLBuilder
import im.vector.matrix.android.api.session.widgets.WidgetURLFormatter
import im.vector.matrix.android.api.util.Cancelable
import javax.inject.Inject
import javax.inject.Provider
internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager,
private val widgetURLBuilder: Provider<WidgetURLBuilder>,
private val widgetPostAPIMediator: Provider<WidgetPostAPIMediator>) : WidgetService {
private val widgetURLFormatter: WidgetURLFormatter,
private val widgetPostAPIMediator: WidgetPostAPIMediator) : WidgetService {
override fun getWidgetURLBuilder(): WidgetURLBuilder {
return widgetURLBuilder.get()
override fun getWidgetURLFormatter(): WidgetURLFormatter {
return widgetURLFormatter
}
override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator {
return widgetPostAPIMediator.get()
return widgetPostAPIMediator
}
override fun getRoomWidgets(roomId: String, widgetId: QueryStringValue, widgetTypes: Set<String>?, excludedTypes: Set<String>?): List<Widget> {

View file

@ -17,20 +17,20 @@
package im.vector.matrix.android.internal.session.widgets
import im.vector.matrix.android.R
import im.vector.matrix.android.api.session.widgets.WidgetURLBuilder
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig
import im.vector.matrix.android.api.session.widgets.WidgetURLFormatter
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig
import im.vector.matrix.android.internal.session.widgets.token.GetScalarTokenTask
import im.vector.matrix.android.internal.util.StringProvider
import java.net.URLEncoder
import javax.inject.Inject
@SessionScope
internal class DefaultWidgetURLBuilder @Inject constructor(private val integrationManager: IntegrationManager,
private val getScalarTokenTask: GetScalarTokenTask,
private val stringProvider: StringProvider
) : IntegrationManager.Listener, WidgetURLBuilder {
internal class DefaultWidgetURLFormatter @Inject constructor(private val integrationManager: IntegrationManager,
private val getScalarTokenTask: GetScalarTokenTask,
private val stringProvider: StringProvider
) : IntegrationManager.Listener, WidgetURLFormatter {
private var currentConfig = integrationManager.getPreferredConfig()
private var whiteListedUrls: List<String> = emptyList()
@ -64,13 +64,13 @@ internal class DefaultWidgetURLBuilder @Inject constructor(private val integrati
/**
* Takes care of fetching a scalar token if required and build the final url.
*/
override suspend fun build(baseUrl: String, params: Map<String, String>, forceFetchScalarToken: Boolean): String {
return if (isScalarUrl(baseUrl) || forceFetchScalarToken) {
val taskParams = GetScalarTokenTask.Params(baseUrl)
override suspend fun format(baseUrl: String, params: Map<String, String>, forceFetchScalarToken: Boolean, bypassWhitelist: Boolean): String {
return if (bypassWhitelist || isWhiteListed(baseUrl)) {
val taskParams = GetScalarTokenTask.Params(currentConfig.apiUrl, forceFetchScalarToken)
val scalarToken = getScalarTokenTask.execute(taskParams)
buildString {
append(baseUrl)
append("scalar_token", scalarToken)
appendParamToUrl("scalar_token", scalarToken)
appendParamsToUrl(params)
}
} else {
@ -81,7 +81,7 @@ internal class DefaultWidgetURLBuilder @Inject constructor(private val integrati
}
}
private fun isScalarUrl(url: String): Boolean {
private fun isWhiteListed(url: String): Boolean {
val allowed: List<String> = whiteListedUrls
for (allowedUrl in allowed) {
if (url.startsWith(allowedUrl)) {

View file

@ -21,7 +21,7 @@ import javax.inject.Inject
internal class WidgetDependenciesHolder @Inject constructor(private val integrationManager: IntegrationManager,
private val widgetManager: WidgetManager,
private val widgetURLBuilder: DefaultWidgetURLBuilder) {
private val widgetURLBuilder: DefaultWidgetURLFormatter) {
fun start() {
integrationManager.start()
@ -30,8 +30,8 @@ internal class WidgetDependenciesHolder @Inject constructor(private val integrat
}
fun stop() {
integrationManager.stop()
widgetManager.stop()
widgetURLBuilder.stop()
widgetManager.stop()
integrationManager.stop()
}
}

View file

@ -21,4 +21,5 @@ import im.vector.matrix.android.api.failure.Failure
sealed class WidgetManagementFailure : Failure.FeatureFailure() {
object NotEnoughPower : WidgetManagementFailure()
object CreationFailed : WidgetManagementFailure()
data class TermsNotSignedException(val baseUrl: String, val token: String) : WidgetManagementFailure()
}

View file

@ -21,7 +21,7 @@ import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator
import im.vector.matrix.android.api.session.widgets.WidgetService
import im.vector.matrix.android.api.session.widgets.WidgetURLBuilder
import im.vector.matrix.android.api.session.widgets.WidgetURLFormatter
import im.vector.matrix.android.internal.session.widgets.token.DefaultGetScalarTokenTask
import im.vector.matrix.android.internal.session.widgets.token.GetScalarTokenTask
import retrofit2.Retrofit
@ -42,7 +42,7 @@ internal abstract class WidgetModule {
abstract fun bindWidgetService(widgetService: DefaultWidgetService): WidgetService
@Binds
abstract fun bindWidgetURLBuilder(widgetURLBuilder: DefaultWidgetURLBuilder): WidgetURLBuilder
abstract fun bindWidgetURLBuilder(widgetURLBuilder: DefaultWidgetURLFormatter): WidgetURLFormatter
@Binds
abstract fun bindWidgetPostAPIMediator(widgetPostMessageAPIProvider: DefaultWidgetPostAPIMediator): WidgetPostAPIMediator

View file

@ -17,19 +17,22 @@
package im.vector.matrix.android.internal.session.widgets.token
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
import im.vector.matrix.android.internal.session.widgets.RegisterWidgetResponse
import im.vector.matrix.android.internal.session.widgets.WidgetManagementFailure
import im.vector.matrix.android.internal.session.widgets.WidgetsAPI
import im.vector.matrix.android.internal.session.widgets.WidgetsAPIProvider
import im.vector.matrix.android.internal.task.Task
import java.net.HttpURLConnection
import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection
internal interface GetScalarTokenTask : Task<GetScalarTokenTask.Params, String> {
data class Params(
val serverUrl: String
val serverUrl: String,
val forceRefresh: Boolean = false
)
}
@ -41,11 +44,16 @@ internal class DefaultGetScalarTokenTask @Inject constructor(private val widgets
override suspend fun execute(params: GetScalarTokenTask.Params): String {
val widgetsAPI = widgetsAPIProvider.get(params.serverUrl)
val scalarToken = scalarTokenStore.getToken(params.serverUrl)
return if (scalarToken == null) {
return if (params.forceRefresh) {
scalarTokenStore.clearToken(params.serverUrl)
getNewScalarToken(widgetsAPI, params.serverUrl)
} else {
validateToken(widgetsAPI, params.serverUrl, scalarToken)
val scalarToken = scalarTokenStore.getToken(params.serverUrl)
if (scalarToken == null) {
getNewScalarToken(widgetsAPI, params.serverUrl)
} else {
validateToken(widgetsAPI, params.serverUrl, scalarToken)
}
}
}
@ -65,19 +73,21 @@ internal class DefaultGetScalarTokenTask @Inject constructor(private val widgets
private suspend fun validateToken(widgetsAPI: WidgetsAPI, serverUrl: String, scalarToken: String): String {
return try {
widgetsAPI.validateToken(scalarToken, WIDGET_API_VERSION)
executeRequest<Unit>(null) {
apiCall = widgetsAPI.validateToken(scalarToken, WIDGET_API_VERSION)
}
scalarToken
} catch (failure: Throwable) {
if (failure.isScalarTokenError()) {
scalarTokenStore.clearToken(serverUrl)
getNewScalarToken(widgetsAPI, serverUrl)
if (failure is Failure.ServerError && failure.httpCode == HttpsURLConnection.HTTP_FORBIDDEN) {
if (failure.error.code == MatrixError.M_TERMS_NOT_SIGNED) {
throw WidgetManagementFailure.TermsNotSignedException(serverUrl, scalarToken)
} else {
scalarTokenStore.clearToken(serverUrl)
getNewScalarToken(widgetsAPI, serverUrl)
}
} else {
throw failure
}
}
}
private fun Throwable.isScalarTokenError(): Boolean {
return this is Failure.ServerError && this.httpCode == HttpURLConnection.HTTP_FORBIDDEN
}
}

View file

@ -29,7 +29,7 @@ internal class ScalarTokenStore @Inject constructor(private val monarchy: Monarc
return monarchy.fetchCopyMap({ realm ->
ScalarTokenEntity.where(realm, apiUrl).findFirst()
}, { scalarToken, _ ->
scalarToken.serverUrl
scalarToken.token
})
}

View file

@ -162,6 +162,7 @@
android:theme="@style/AppTheme.AttachmentsPreview" />
<activity android:name=".features.terms.ReviewTermsActivity" />
<activity android:name=".features.widgets.WidgetActivity" />
<!-- Services -->

View file

@ -434,18 +434,24 @@ class RoomDetailFragment @Inject constructor(
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.clear_message_queue) {
// This a temporary option during dev as it is not super stable
// Cancel all pending actions in room queue and post a dummy
// Then mark all sending events as undelivered
roomDetailViewModel.handle(RoomDetailAction.ClearSendQueue)
return true
return when (item.itemId) {
R.id.clear_message_queue -> {
// This a temporary option during dev as it is not super stable
// Cancel all pending actions in room queue and post a dummy
// Then mark all sending events as undelivered
roomDetailViewModel.handle(RoomDetailAction.ClearSendQueue)
true
}
R.id.resend_all -> {
roomDetailViewModel.handle(RoomDetailAction.ResendAll)
true
}
R.id.open_matrix_apps -> {
navigator.openIntegrationManager(requireContext(), roomDetailArgs.roomId, null, null)
true
}
else -> super.onOptionsItemSelected(item)
}
if (item.itemId == R.id.resend_all) {
roomDetailViewModel.handle(RoomDetailAction.ResendAll)
return true
}
return super.onOptionsItemSelected(item)
}
private fun renderRegularMode(text: String) {

View file

@ -320,6 +320,7 @@ class RoomDetailViewModel @AssistedInject constructor(
timeline.pendingEventCount() > 0 && vectorPreferences.developerMode()
R.id.resend_all -> timeline.failedToDeliverEventCount() > 0
R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
R.id.open_matrix_apps -> session.integrationManagerService().isIntegrationEnabled()
else -> false
}

View file

@ -16,6 +16,7 @@
package im.vector.riotx.features.home.room.detail
import android.util.SparseArray
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
@ -63,6 +64,7 @@ data class RoomDetailViewState(
val syncState: SyncState = SyncState.Idle,
val highlightedEventId: String? = null,
val unreadState: UnreadState = UnreadState.Unknown,
val menuItemsVisibility: SparseArray<Boolean> = SparseArray(4),
val canShowJumpToReadMarker: Boolean = true
) : MvRxState {

View file

@ -55,6 +55,8 @@ import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity
import im.vector.riotx.features.widgets.WidgetActivity
import im.vector.riotx.features.widgets.WidgetArgsBuilder
import javax.inject.Inject
import javax.inject.Singleton
@ -62,6 +64,7 @@ import javax.inject.Singleton
class DefaultNavigator @Inject constructor(
private val sessionHolder: ActiveSessionHolder,
private val vectorPreferences: VectorPreferences,
private val widgetArgsBuilder: WidgetArgsBuilder,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider
) : Navigator {
@ -215,8 +218,9 @@ class DefaultNavigator @Inject constructor(
fragment.startActivityForResult(intent, requestCode)
}
override fun openIntegrationManager(context: Context) {
//TODO
override fun openIntegrationManager(context: Context, roomId: String, integId: String?, screenId: String?) {
val widgetArgs = widgetArgsBuilder.buildIntegrationManagerArgs(roomId, integId, screenId)
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
}
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {

View file

@ -77,6 +77,6 @@ interface Navigator {
token: String?,
requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE)
fun openIntegrationManager(context: Context, integId: String?, integType: String?)
fun openIntegrationManager(context: Context, roomId: String, integId: String?, screenId: String?)
}

View file

@ -23,8 +23,10 @@ import android.graphics.Bitmap
import android.os.Build
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.RequiresApi
/**
* This class inherits from WebViewClient. It has to be used with a WebView.
@ -56,6 +58,14 @@ class VectorWebViewClient(private val eventListener: WebViewEventListener) : Web
}
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
super.onReceivedHttpError(view, request, errorResponse)
eventListener.onHttpError(request.url.toString(),
errorResponse.statusCode,
errorResponse.reasonPhrase)
}
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
super.onReceivedError(view, errorCode, description, failingUrl)
if (!mInError) {

View file

@ -56,6 +56,17 @@ interface WebViewEventListener {
//NO-OP
}
/**
* Triggered when an error occurred while loading a page.
*
* @param url The url that failed.
* @param errorCode The error code.
* @param description The error description.
*/
fun onHttpError(url: String, errorCode: Int, description: String){
//NO-OP
}
/**
* Triggered when a webview load an url
*

View file

@ -0,0 +1,55 @@
/*
* 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.riotx.features.widgets
import android.content.Context
import android.content.Intent
import androidx.appcompat.widget.Toolbar
import im.vector.riotx.R
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.widgets.room.RoomWidgetFragment
import im.vector.riotx.features.widgets.room.WidgetArgs
class WidgetActivity : VectorBaseActivity(), ToolbarConfigurable {
companion object {
private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS"
fun newIntent(context: Context, args: WidgetArgs): Intent {
return Intent(context, WidgetActivity::class.java).apply {
putExtra(EXTRA_FRAGMENT_ARGS, args)
}
}
}
override fun getLayoutRes() = R.layout.activity_simple
override fun initUiAndData() {
if (isFirstCreation()) {
val fragmentArgs: WidgetArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS)
?: return
addFragment(R.id.simpleFragmentContainer, RoomWidgetFragment::class.java, fragmentArgs)
}
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}
}

View file

@ -0,0 +1,41 @@
/*
* 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.riotx.features.widgets
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.features.widgets.room.WidgetArgs
import im.vector.riotx.features.widgets.room.WidgetKind
import javax.inject.Inject
class WidgetArgsBuilder @Inject constructor(private val sessionHolder: ActiveSessionHolder) {
@Suppress("UNCHECKED_CAST")
fun buildIntegrationManagerArgs(roomId: String, integId: String?, screenId: String?): WidgetArgs {
val session = sessionHolder.getActiveSession()
val integrationManagerConfig = session.integrationManagerService().getPreferredConfig()
return WidgetArgs(
baseUrl = integrationManagerConfig.uiUrl,
kind = WidgetKind.INTEGRATION_MANAGER,
roomId = roomId,
urlParams = mapOf(
"screen" to screenId,
"integ_id" to integId,
"room_id" to roomId
).filterValues { it != null } as Map<String, String>
)
}
}

View file

@ -18,10 +18,13 @@ package im.vector.riotx.features.widgets
import android.content.Context
import android.text.TextUtils
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
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.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
@ -32,15 +35,21 @@ import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.navigation.Navigator
import timber.log.Timber
import java.util.ArrayList
import java.util.HashMap
class WidgetPostAPIHandler(private val context: Context,
private val roomId: String,
private val navigator: Navigator,
private val stringProvider: StringProvider,
private val widgetPostAPIMediator: WidgetPostAPIMediator,
private val session: Session) : WidgetPostAPIMediator.Handler {
class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roomId: String,
private val context: Context,
private val navigator: Navigator,
private val stringProvider: StringProvider,
private val session: Session) : WidgetPostAPIMediator.Handler {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): WidgetPostAPIHandler
}
private val widgetPostAPIMediator = session.widgetService().getWidgetPostAPIMediator()
private val room = session.getRoom(roomId)!!
override fun handleWidgetRequest(eventData: JsonDict): Boolean {
@ -50,7 +59,7 @@ class WidgetPostAPIHandler(private val context: Context,
"can_send_event" -> canSendEvent(eventData).run { true }
//"close_scalar" -> finish().run { true }
"get_membership_count" -> getMembershipCount(eventData).run { true }
//"get_widgets" -> getWidgets(eventData).run { true }
"get_widgets" -> getWidgets(eventData).run { true }
//"invite" -> inviteUser(eventData).run { true }
"join_rules_state" -> getJoinRules(eventData).run { true }
"membership_state" -> getMembershipState(eventData).run { true }
@ -82,7 +91,7 @@ class WidgetPostAPIHandler(private val context: Context,
// Add "type_" as a prefix
integType?.let { integType = "type_$integType" }
}
navigator.openIntegrationManager(context, integId, integType)
navigator.openIntegrationManager(context, roomId, integId, integType)
}
/**
@ -158,6 +167,11 @@ class WidgetPostAPIHandler(private val context: Context,
val userId = eventData["user_id"] as String
Timber.d("membership_state of $userId in room $roomId requested")
val roomMemberStateEvent = room.getStateEvent(EventType.STATE_ROOM_MEMBER, stateKey = QueryStringValue.Equals(userId, QueryStringValue.Case.SENSITIVE))
if (roomMemberStateEvent != null) {
widgetPostAPIMediator.sendObjectResponse(Map::class.java, roomMemberStateEvent.content, eventData)
} else {
widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData)
}
}
/**
@ -178,6 +192,27 @@ class WidgetPostAPIHandler(private val context: Context,
}
}
/**
* Provide the widgets list
*
* @param eventData the modular data
*/
private fun getWidgets(eventData: JsonDict) {
if (checkRoomId(eventData)) {
return
}
Timber.d("Received request to get widget in room $roomId")
val roomWidgets = session.widgetService().getRoomWidgets(roomId)
val responseData = ArrayList<JsonDict>()
for (widget in roomWidgets) {
val map = widget.event.toContent()
responseData.add(map)
}
// TODO ADD USER WIDGETS
Timber.d("## getWidgets() returns $responseData")
widgetPostAPIMediator.sendObjectResponse(List::class.java, responseData, eventData)
}
/**
* Update the 'plumbing state"
*
@ -207,6 +242,7 @@ class WidgetPostAPIHandler(private val context: Context,
*
* @param eventData the modular data
*/
@Suppress("UNCHECKED_CAST")
private fun setBotOptions(eventData: JsonDict) {
if (checkRoomId(eventData) || checkUserId(eventData)) {
return

View file

@ -20,6 +20,7 @@ import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomWidgetAction : VectorViewModelAction {
data class OnWebViewStartedToLoad(val url: String) : RoomWidgetAction()
data class OnWebViewLoadingError(val url: String) : RoomWidgetAction()
data class OnWebViewLoadingError(val url: String, val isHttpError: Boolean, val errorCode: Int, val errorDescription: String) : RoomWidgetAction()
data class OnWebViewLoadingSuccess(val url: String) : RoomWidgetAction()
object OnTermsReviewed: RoomWidgetAction()
}

View file

@ -16,16 +16,24 @@
package im.vector.riotx.features.widgets.room
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.terms.ReviewTermsActivity
import im.vector.riotx.features.webview.WebViewEventListener
import im.vector.riotx.features.widgets.webview.clearAfterWidget
import im.vector.riotx.features.widgets.webview.setupForWidget
@ -39,12 +47,13 @@ data class WidgetArgs(
val baseUrl: String,
val kind: WidgetKind,
val roomId: String,
val widgetId: String? = null
val widgetId: String? = null,
val urlParams: Map<String, String> = emptyMap()
) : Parcelable
class RoomWidgetFragment @Inject constructor(
private val viewModelFactory: RoomWidgetViewModel.Factory
) : VectorBaseFragment(), RoomWidgetViewModel.Factory by viewModelFactory, WebViewEventListener, WidgetPostAPIMediator.Handler {
) : VectorBaseFragment(), RoomWidgetViewModel.Factory by viewModelFactory, WebViewEventListener {
private val fragmentArgs: WidgetArgs by args()
private val viewModel: RoomWidgetViewModel by fragmentViewModel()
@ -54,12 +63,31 @@ class RoomWidgetFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
widgetWebView.setupForWidget(this)
viewModel.getPostAPIMediator().initialize(widgetWebView, this)
if (fragmentArgs.kind.isAdmin()) {
viewModel.getPostAPIMediator().setWebView(widgetWebView)
}
viewModel.observeViewEvents {
when (it) {
is RoomWidgetViewEvents.DisplayTerms -> displayTerms(it)
is RoomWidgetViewEvents.LoadFormattedURL -> loadFormattedUrl(it)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == ReviewTermsActivity.TERMS_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
viewModel.handle(RoomWidgetAction.OnTermsReviewed)
} else {
vectorBaseActivity.finish()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
viewModel.getPostAPIMediator().clear()
if (fragmentArgs.kind.isAdmin()) {
viewModel.getPostAPIMediator().clearWebView()
}
widgetWebView.clearAfterWidget()
}
@ -80,21 +108,99 @@ class RoomWidgetFragment @Inject constructor(
}
override fun invalidate() = withState(viewModel) { state ->
Timber.v("Invalidate with state: $state")
Timber.v("Invalidate state: $state")
when (state.status) {
WidgetStatus.UNKNOWN -> {
//Hide all?
widgetWebView.isVisible = false
}
WidgetStatus.WIDGET_NOT_ALLOWED -> {
widgetWebView.isVisible = false
}
WidgetStatus.WIDGET_ALLOWED -> {
widgetWebView.isVisible = true
when (state.formattedURL) {
Uninitialized -> {
}
is Loading -> {
setStateError(null)
widgetProgressBar.isIndeterminate = true
widgetProgressBar.isVisible = true
}
is Success -> {
setStateError(null)
when (state.webviewLoadedUrl) {
Uninitialized -> {
widgetWebView.isInvisible = true
}
is Loading -> {
setStateError(null)
widgetWebView.isInvisible = false
widgetProgressBar.isIndeterminate = true
widgetProgressBar.isVisible = true
}
is Success -> {
widgetWebView.isInvisible = false
widgetProgressBar.isVisible = false
setStateError(null)
}
is Fail -> {
widgetProgressBar.isInvisible = true
setStateError(state.webviewLoadedUrl.error.message)
}
}
}
is Fail -> {
//we need to show Error
widgetWebView.isInvisible = true
widgetProgressBar.isVisible = false
setStateError(state.formattedURL.error.message)
}
}
}
}
}
private fun displayTerms(displayTerms: RoomWidgetViewEvents.DisplayTerms) {
navigator.openTerms(
fragment = this,
serviceType = TermsService.ServiceType.IntegrationManager,
baseUrl = displayTerms.url,
token = displayTerms.token
)
}
private fun loadFormattedUrl(loadFormattedUrl: RoomWidgetViewEvents.LoadFormattedURL) {
widgetWebView.clearHistory()
widgetWebView.loadUrl(loadFormattedUrl.formattedURL)
}
private fun setStateError(message: String?) {
if (message == null) {
widgetErrorLayout.isVisible = false
widgetErrorText.text = null
} else {
widgetProgressBar.isVisible = false
widgetErrorLayout.isVisible = true
widgetWebView.isInvisible = true
widgetErrorText.text = getString(R.string.room_widget_failed_to_load, message)
}
}
override fun onPageStarted(url: String) {
viewModel.handle(RoomWidgetAction.OnWebViewStartedToLoad(url))
}
override fun onPageFinished(url: String) {
viewModel.handle(RoomWidgetAction.OnWebViewLoadingSuccess(url))
}
override fun onPageError(url: String, errorCode: Int, description: String) {
viewModel.handle(RoomWidgetAction.OnWebViewLoadingError(url, false, errorCode, description))
}
override fun handleWidgetRequest(eventData: JsonDict): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
override fun onHttpError(url: String, errorCode: Int, description: String) {
viewModel.handle(RoomWidgetAction.OnWebViewLoadingError(url, true, errorCode, description))
}
}

View file

@ -18,4 +18,7 @@ package im.vector.riotx.features.widgets.room
import im.vector.riotx.core.platform.VectorViewEvents
sealed class RoomWidgetViewEvents : VectorViewEvents
sealed class RoomWidgetViewEvents : VectorViewEvents {
data class LoadFormattedURL(val formattedURL: String): RoomWidgetViewEvents()
data class DisplayTerms(val url: String, val token: String): RoomWidgetViewEvents()
}

View file

@ -28,10 +28,15 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.session.widgets.WidgetManagementFailure
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.widgets.WidgetPostAPIHandler
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.net.ssl.HttpsURLConnection
class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState: WidgetViewState,
private val widgetPostAPIHandlerFactory: WidgetPostAPIHandler.Factory,
private val session: Session)
: VectorViewModel<WidgetViewState, RoomWidgetAction, RoomWidgetViewEvents>(initialState) {
@ -54,76 +59,119 @@ class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState
private val widgetService = session.widgetService()
private val integrationManagerService = session.integrationManagerService()
private val widgetBuilder = widgetService.getWidgetURLBuilder()
private val widgetBuilder = widgetService.getWidgetURLFormatter()
private val postAPIMediator = widgetService.getWidgetPostAPIMediator()
init {
if(initialState.widgetKind.isAdmin()) {
val widgetPostAPIHandler = widgetPostAPIHandlerFactory.create(initialState.roomId)
postAPIMediator.setHandler(widgetPostAPIHandler)
}
refreshPermissionStatus()
observePermissionStatus()
}
private fun observePermissionStatus() {
selectSubscribe(WidgetViewState::status) {
Timber.v("Widget status: $it")
if (it == WidgetStatus.WIDGET_ALLOWED) {
loadFormattedUrl()
}
}
}
fun getPostAPIMediator() = postAPIMediator
override fun handle(action: RoomWidgetAction) {
when (action) {
is RoomWidgetAction.OnWebViewLoadingError -> handleWebViewLoadingError(action.url)
is RoomWidgetAction.OnWebViewLoadingError -> handleWebViewLoadingError(action.isHttpError, action.errorCode, action.errorDescription)
is RoomWidgetAction.OnWebViewLoadingSuccess -> handleWebViewLoadingSuccess(action.url)
is RoomWidgetAction.OnWebViewStartedToLoad -> handleWebViewStartLoading(action.url)
is RoomWidgetAction.OnWebViewStartedToLoad -> handleWebViewStartLoading()
}
}
private fun refreshPermissionStatus() {
if (initialState.widgetKind == WidgetKind.USER || initialState.widgetKind == WidgetKind.INTEGRATION_MANAGER) {
onWidgetAllowed()
if (initialState.widgetKind.isAdmin()) {
setWidgetStatus(WidgetStatus.WIDGET_ALLOWED)
} else {
val widgetId = initialState.widgetId
if (widgetId == null) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) }
setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED)
return
}
val roomWidget = widgetService.getRoomWidgets(initialState.roomId, widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.SENSITIVE)).firstOrNull()
if (roomWidget == null) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) }
setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED)
return
}
if (roomWidget.event?.senderId == session.myUserId) {
onWidgetAllowed()
setWidgetStatus(WidgetStatus.WIDGET_ALLOWED)
} else {
val stateEventId = roomWidget.event?.eventId
// This should not happen
if (stateEventId == null) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) }
setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED)
return
}
val isAllowed = integrationManagerService.isWidgetAllowed(stateEventId)
if (!isAllowed) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) }
setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED)
} else {
onWidgetAllowed()
setWidgetStatus(WidgetStatus.WIDGET_ALLOWED)
}
}
}
}
private fun onWidgetAllowed() {
setState {
copy(status = WidgetStatus.WIDGET_ALLOWED, formattedURL = Loading())
}
private fun setWidgetStatus(widgetStatus: WidgetStatus) {
setState { copy(status = widgetStatus) }
}
private fun loadFormattedUrl(forceFetchToken: Boolean = false) {
viewModelScope.launch {
try {
val formattedUrl = widgetBuilder.build(initialState.baseUrl)
setState { copy(formattedURL = Loading()) }
val formattedUrl = widgetBuilder.format(
baseUrl = initialState.baseUrl,
params = initialState.urlParams,
forceFetchScalarToken = forceFetchToken,
bypassWhitelist = initialState.widgetKind == WidgetKind.INTEGRATION_MANAGER
)
setState { copy(formattedURL = Success(formattedUrl)) }
_viewEvents.post(RoomWidgetViewEvents.LoadFormattedURL(formattedUrl))
} catch (failure: Throwable) {
if (failure is WidgetManagementFailure.TermsNotSignedException) {
_viewEvents.post(RoomWidgetViewEvents.DisplayTerms(failure.baseUrl, failure.token))
}
setState { copy(formattedURL = Fail(failure)) }
}
}
}
private fun handleWebViewStartLoading(url: String) {
private fun handleWebViewStartLoading() {
setState { copy(webviewLoadedUrl = Loading()) }
}
private fun handleWebViewLoadingSuccess(url: String) {
if (initialState.widgetKind.isAdmin()) {
postAPIMediator.injectAPI()
}
setState { copy(webviewLoadedUrl = Success(url)) }
}
private fun handleWebViewLoadingError(url: String) {
private fun handleWebViewLoadingError(isHttpError: Boolean, reason: Int, errorDescription: String) {
if (isHttpError) {
// In case of 403, try to refresh the scalar token
if (reason == HttpsURLConnection.HTTP_FORBIDDEN) {
loadFormattedUrl(true)
}
} else {
setState { copy(webviewLoadedUrl = Fail(Throwable(errorDescription))) }
}
}
override fun onCleared() {
super.onCleared()
postAPIMediator.setHandler(null)
}
}

View file

@ -29,12 +29,17 @@ enum class WidgetStatus {
enum class WidgetKind {
ROOM,
USER,
INTEGRATION_MANAGER
INTEGRATION_MANAGER;
fun isAdmin(): Boolean {
return this == USER || this == INTEGRATION_MANAGER
}
}
data class WidgetViewState(
val roomId: String,
val baseUrl: String,
val urlParams: Map<String, String> = emptyMap(),
val widgetId: String? = null,
val widgetKind: WidgetKind,
val status: WidgetStatus = WidgetStatus.UNKNOWN,
@ -49,6 +54,7 @@ data class WidgetViewState(
widgetKind = widgetArgs.kind,
baseUrl = widgetArgs.baseUrl,
roomId = widgetArgs.roomId,
widgetId = widgetArgs.widgetId
widgetId = widgetArgs.widgetId,
urlParams = widgetArgs.urlParams
)
}

View file

@ -1,42 +0,0 @@
/*
* 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.riotx.features.widgets.room
interface WidgetParams {
val params: Map<String, String>
}
class IntegrationManagerParams(
private val widgetId: String? = null,
private val screenId: String? = null) : WidgetParams {
override val params: Map<String, String> by lazy {
buildParams()
}
private fun buildParams(): Map<String, String> {
val map = HashMap<String, String>()
if (widgetId != null) {
map["integ_id"] = widgetId
}
if (screenId != null) {
map["screen"] = screenId
}
return map
}
}

View file

@ -3,6 +3,11 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/open_matrix_apps"
android:title="@string/room_add_matrix_apps"
app:showAsAction="never" />
<item
android:id="@+id/resend_all"
android:icon="@drawable/ic_refresh_cw"