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 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 OutdatedIdentityServer : IdentityServiceError()
object OutdatedHomeServer : IdentityServiceError() object OutdatedHomeServer : IdentityServiceError()
object NoIdentityServerConfigured : IdentityServiceError() object NoIdentityServerConfigured : IdentityServiceError()

View file

@ -18,20 +18,27 @@ package im.vector.matrix.android.api.session.widgets
import android.webkit.WebView import android.webkit.WebView
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import java.lang.reflect.Type
interface WidgetPostAPIMediator { interface WidgetPostAPIMediator {
/** /**
* This initialize the mediator and configure the webview. * This initialize the webview to handle.
* It will add a JavaScript Interface. * 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. * This clear the mediator by removing the JavaScript Interface and cleaning references.
*/ */
fun clear() fun clearWebView()
/** /**
* Inject the necessary javascript into the configured WebView. * Inject the necessary javascript into the configured WebView.
@ -62,7 +69,7 @@ interface WidgetPostAPIMediator {
* @param response the response * @param response the response
* @param eventData the modular data * @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 * Send success

View file

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

View file

@ -16,10 +16,16 @@
package im.vector.matrix.android.api.session.widgets 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. * 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_PREFIX_PATH = "_matrix/identity/v2"
const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/" const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/"
// TODO Ganfra, use correct value const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/"
const val URI_INTEGRATION_MANAGER_PATH = "TODO/"
} }

View file

@ -28,8 +28,14 @@ import javax.inject.Inject
class RetrofitFactory @Inject constructor(private val moshi: Moshi) { class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
fun create(okHttpClient: Lazy<OkHttpClient>, baseUrl: String): Retrofit { fun create(okHttpClient: Lazy<OkHttpClient>, baseUrl: String): Retrofit {
// ensure trailing /
val safeBaseUrl = if (!baseUrl.endsWith("/")) {
"$baseUrl/"
} else {
baseUrl
}
return Retrofit.Builder() return Retrofit.Builder()
.baseUrl(baseUrl) .baseUrl(safeBaseUrl)
.callFactory(object : Call.Factory { .callFactory(object : Call.Factory {
override fun newCall(request: Request): Call { override fun newCall(request: Request): Call {
return okHttpClient.get().newCall(request) 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")) callback.onFailure(InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported"))
} }
else -> { else -> {
val params = SendStateTask.Params(roomId, val params = SendStateTask.Params(
EventType.STATE_ROOM_ENCRYPTION, roomId = roomId,
mapOf( eventType = EventType.STATE_ROOM_ENCRYPTION,
body = mapOf(
"algorithm" to algorithm "algorithm" to algorithm
)) ))

View file

@ -79,7 +79,8 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
return sendStateEvent( return sendStateEvent(
eventType = EventType.STATE_ROOM_TOPIC, eventType = EventType.STATE_ROOM_TOPIC,
body = mapOf("topic" to 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.session.widgets.WidgetPostAPIMediator
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.util.createUIHandler
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.Type
import java.util.HashMap import java.util.HashMap
import javax.inject.Inject import javax.inject.Inject
@ -35,22 +37,28 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
private var handler: WidgetPostAPIMediator.Handler? = null private var handler: WidgetPostAPIMediator.Handler? = null
private var webView: WebView? = 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.webView = webView
this.handler = handler webView.addJavascriptInterface(this, "Android")
webView.addJavascriptInterface(this, "WidgetPostAPIMediator")
} }
override fun clear() { override fun clearWebView() {
handler = null webView?.removeJavascriptInterface("Android")
webView?.removeJavascriptInterface("WidgetPostAPIMediator")
webView = null webView = null
} }
override fun setHandler(handler: WidgetPostAPIMediator.Handler?) {
this.handler = handler
}
override fun injectAPI() { override fun injectAPI() {
val js = widgetPostMessageAPIProvider.get() val js = widgetPostMessageAPIProvider.get()
if (null != js) { if (js != null) {
webView?.loadUrl("javascript:$js") uiHandler.post {
webView?.loadUrl("javascript:$js")
}
} }
} }
@ -111,10 +119,10 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
* @param response the response * @param response the response
* @param eventData the modular data * @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 var jsString: String? = null
if (response != null) { if (response != null) {
val objectAdapter = moshi.adapter(klass) val objectAdapter = moshi.adapter<T>(type)
try { try {
jsString = "JSON.parse('${objectAdapter.toJson(response)}')" jsString = "JSON.parse('${objectAdapter.toJson(response)}')"
} catch (e: Exception) { } catch (e: Exception) {
@ -157,7 +165,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
* @param jsString the response data * @param jsString the response data
* @param eventData the modular data * @param eventData the modular data
*/ */
private fun sendResponse(jsString: String, eventData: JsonDict) { private fun sendResponse(jsString: String, eventData: JsonDict) = uiHandler.post {
try { try {
val functionLine = "sendResponseFromRiotAndroid('" + eventData["_id"] + "' , " + jsString + ");" val functionLine = "sendResponseFromRiotAndroid('" + eventData["_id"] + "' , " + jsString + ");"
Timber.v("BRIDGE sendResponse: $functionLine") 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.events.model.Content
import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator 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.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 im.vector.matrix.android.api.util.Cancelable
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager, internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager,
private val widgetURLBuilder: Provider<WidgetURLBuilder>, private val widgetURLFormatter: WidgetURLFormatter,
private val widgetPostAPIMediator: Provider<WidgetPostAPIMediator>) : WidgetService { private val widgetPostAPIMediator: WidgetPostAPIMediator) : WidgetService {
override fun getWidgetURLBuilder(): WidgetURLBuilder { override fun getWidgetURLFormatter(): WidgetURLFormatter {
return widgetURLBuilder.get() return widgetURLFormatter
} }
override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator { override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator {
return widgetPostAPIMediator.get() return widgetPostAPIMediator
} }
override fun getRoomWidgets(roomId: String, widgetId: QueryStringValue, widgetTypes: Set<String>?, excludedTypes: Set<String>?): List<Widget> { 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 package im.vector.matrix.android.internal.session.widgets
import im.vector.matrix.android.R 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.SessionScope
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager 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.session.widgets.token.GetScalarTokenTask
import im.vector.matrix.android.internal.util.StringProvider import im.vector.matrix.android.internal.util.StringProvider
import java.net.URLEncoder import java.net.URLEncoder
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
internal class DefaultWidgetURLBuilder @Inject constructor(private val integrationManager: IntegrationManager, internal class DefaultWidgetURLFormatter @Inject constructor(private val integrationManager: IntegrationManager,
private val getScalarTokenTask: GetScalarTokenTask, private val getScalarTokenTask: GetScalarTokenTask,
private val stringProvider: StringProvider private val stringProvider: StringProvider
) : IntegrationManager.Listener, WidgetURLBuilder { ) : IntegrationManager.Listener, WidgetURLFormatter {
private var currentConfig = integrationManager.getPreferredConfig() private var currentConfig = integrationManager.getPreferredConfig()
private var whiteListedUrls: List<String> = emptyList() 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. * 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 { override suspend fun format(baseUrl: String, params: Map<String, String>, forceFetchScalarToken: Boolean, bypassWhitelist: Boolean): String {
return if (isScalarUrl(baseUrl) || forceFetchScalarToken) { return if (bypassWhitelist || isWhiteListed(baseUrl)) {
val taskParams = GetScalarTokenTask.Params(baseUrl) val taskParams = GetScalarTokenTask.Params(currentConfig.apiUrl, forceFetchScalarToken)
val scalarToken = getScalarTokenTask.execute(taskParams) val scalarToken = getScalarTokenTask.execute(taskParams)
buildString { buildString {
append(baseUrl) append(baseUrl)
append("scalar_token", scalarToken) appendParamToUrl("scalar_token", scalarToken)
appendParamsToUrl(params) appendParamsToUrl(params)
} }
} else { } 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 val allowed: List<String> = whiteListedUrls
for (allowedUrl in allowed) { for (allowedUrl in allowed) {
if (url.startsWith(allowedUrl)) { if (url.startsWith(allowedUrl)) {

View file

@ -21,7 +21,7 @@ import javax.inject.Inject
internal class WidgetDependenciesHolder @Inject constructor(private val integrationManager: IntegrationManager, internal class WidgetDependenciesHolder @Inject constructor(private val integrationManager: IntegrationManager,
private val widgetManager: WidgetManager, private val widgetManager: WidgetManager,
private val widgetURLBuilder: DefaultWidgetURLBuilder) { private val widgetURLBuilder: DefaultWidgetURLFormatter) {
fun start() { fun start() {
integrationManager.start() integrationManager.start()
@ -30,8 +30,8 @@ internal class WidgetDependenciesHolder @Inject constructor(private val integrat
} }
fun stop() { fun stop() {
integrationManager.stop()
widgetManager.stop()
widgetURLBuilder.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() { sealed class WidgetManagementFailure : Failure.FeatureFailure() {
object NotEnoughPower : WidgetManagementFailure() object NotEnoughPower : WidgetManagementFailure()
object CreationFailed : 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 dagger.Provides
import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator 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.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.DefaultGetScalarTokenTask
import im.vector.matrix.android.internal.session.widgets.token.GetScalarTokenTask import im.vector.matrix.android.internal.session.widgets.token.GetScalarTokenTask
import retrofit2.Retrofit import retrofit2.Retrofit
@ -42,7 +42,7 @@ internal abstract class WidgetModule {
abstract fun bindWidgetService(widgetService: DefaultWidgetService): WidgetService abstract fun bindWidgetService(widgetService: DefaultWidgetService): WidgetService
@Binds @Binds
abstract fun bindWidgetURLBuilder(widgetURLBuilder: DefaultWidgetURLBuilder): WidgetURLBuilder abstract fun bindWidgetURLBuilder(widgetURLBuilder: DefaultWidgetURLFormatter): WidgetURLFormatter
@Binds @Binds
abstract fun bindWidgetPostAPIMediator(widgetPostMessageAPIProvider: DefaultWidgetPostAPIMediator): WidgetPostAPIMediator abstract fun bindWidgetPostAPIMediator(widgetPostMessageAPIProvider: DefaultWidgetPostAPIMediator): WidgetPostAPIMediator

View file

@ -17,19 +17,22 @@
package im.vector.matrix.android.internal.session.widgets.token package im.vector.matrix.android.internal.session.widgets.token
import im.vector.matrix.android.api.failure.Failure 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.network.executeRequest
import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask 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.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.WidgetsAPI
import im.vector.matrix.android.internal.session.widgets.WidgetsAPIProvider import im.vector.matrix.android.internal.session.widgets.WidgetsAPIProvider
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import java.net.HttpURLConnection
import javax.inject.Inject import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection
internal interface GetScalarTokenTask : Task<GetScalarTokenTask.Params, String> { internal interface GetScalarTokenTask : Task<GetScalarTokenTask.Params, String> {
data class Params( 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 { override suspend fun execute(params: GetScalarTokenTask.Params): String {
val widgetsAPI = widgetsAPIProvider.get(params.serverUrl) val widgetsAPI = widgetsAPIProvider.get(params.serverUrl)
val scalarToken = scalarTokenStore.getToken(params.serverUrl) return if (params.forceRefresh) {
return if (scalarToken == null) { scalarTokenStore.clearToken(params.serverUrl)
getNewScalarToken(widgetsAPI, params.serverUrl) getNewScalarToken(widgetsAPI, params.serverUrl)
} else { } 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 { private suspend fun validateToken(widgetsAPI: WidgetsAPI, serverUrl: String, scalarToken: String): String {
return try { return try {
widgetsAPI.validateToken(scalarToken, WIDGET_API_VERSION) executeRequest<Unit>(null) {
apiCall = widgetsAPI.validateToken(scalarToken, WIDGET_API_VERSION)
}
scalarToken scalarToken
} catch (failure: Throwable) { } catch (failure: Throwable) {
if (failure.isScalarTokenError()) { if (failure is Failure.ServerError && failure.httpCode == HttpsURLConnection.HTTP_FORBIDDEN) {
scalarTokenStore.clearToken(serverUrl) if (failure.error.code == MatrixError.M_TERMS_NOT_SIGNED) {
getNewScalarToken(widgetsAPI, serverUrl) throw WidgetManagementFailure.TermsNotSignedException(serverUrl, scalarToken)
} else {
scalarTokenStore.clearToken(serverUrl)
getNewScalarToken(widgetsAPI, serverUrl)
}
} else { } else {
throw failure 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 -> return monarchy.fetchCopyMap({ realm ->
ScalarTokenEntity.where(realm, apiUrl).findFirst() ScalarTokenEntity.where(realm, apiUrl).findFirst()
}, { scalarToken, _ -> }, { scalarToken, _ ->
scalarToken.serverUrl scalarToken.token
}) })
} }

View file

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

View file

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

View file

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

View file

@ -16,6 +16,7 @@
package im.vector.riotx.features.home.room.detail package im.vector.riotx.features.home.room.detail
import android.util.SparseArray
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
@ -63,6 +64,7 @@ data class RoomDetailViewState(
val syncState: SyncState = SyncState.Idle, val syncState: SyncState = SyncState.Idle,
val highlightedEventId: String? = null, val highlightedEventId: String? = null,
val unreadState: UnreadState = UnreadState.Unknown, val unreadState: UnreadState = UnreadState.Unknown,
val menuItemsVisibility: SparseArray<Boolean> = SparseArray(4),
val canShowJumpToReadMarker: Boolean = true val canShowJumpToReadMarker: Boolean = true
) : MvRxState { ) : 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.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity 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.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -62,6 +64,7 @@ import javax.inject.Singleton
class DefaultNavigator @Inject constructor( class DefaultNavigator @Inject constructor(
private val sessionHolder: ActiveSessionHolder, private val sessionHolder: ActiveSessionHolder,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val widgetArgsBuilder: WidgetArgsBuilder,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider
) : Navigator { ) : Navigator {
@ -215,8 +218,9 @@ class DefaultNavigator @Inject constructor(
fragment.startActivityForResult(intent, requestCode) fragment.startActivityForResult(intent, requestCode)
} }
override fun openIntegrationManager(context: Context) { override fun openIntegrationManager(context: Context, roomId: String, integId: String?, screenId: String?) {
//TODO val widgetArgs = widgetArgsBuilder.buildIntegrationManagerArgs(roomId, integId, screenId)
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
} }
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) { private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {

View file

@ -77,6 +77,6 @@ interface Navigator {
token: String?, token: String?,
requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE) 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.os.Build
import android.webkit.WebResourceError import android.webkit.WebResourceError
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.annotation.RequiresApi
/** /**
* This class inherits from WebViewClient. It has to be used with a WebView. * 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) { override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
super.onReceivedError(view, errorCode, description, failingUrl) super.onReceivedError(view, errorCode, description, failingUrl)
if (!mInError) { if (!mInError) {

View file

@ -56,6 +56,17 @@ interface WebViewEventListener {
//NO-OP //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 * 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.content.Context
import android.text.TextUtils 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.query.QueryStringValue
import im.vector.matrix.android.api.session.Session 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.Event
import im.vector.matrix.android.api.session.events.model.EventType 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.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent 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.core.resources.StringProvider
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import timber.log.Timber import timber.log.Timber
import java.util.ArrayList
import java.util.HashMap import java.util.HashMap
class WidgetPostAPIHandler(private val context: Context, class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roomId: String,
private val roomId: String, private val context: Context,
private val navigator: Navigator, private val navigator: Navigator,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val widgetPostAPIMediator: WidgetPostAPIMediator, private val session: Session) : WidgetPostAPIMediator.Handler {
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)!! private val room = session.getRoom(roomId)!!
override fun handleWidgetRequest(eventData: JsonDict): Boolean { override fun handleWidgetRequest(eventData: JsonDict): Boolean {
@ -50,7 +59,7 @@ class WidgetPostAPIHandler(private val context: Context,
"can_send_event" -> canSendEvent(eventData).run { true } "can_send_event" -> canSendEvent(eventData).run { true }
//"close_scalar" -> finish().run { true } //"close_scalar" -> finish().run { true }
"get_membership_count" -> getMembershipCount(eventData).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 } //"invite" -> inviteUser(eventData).run { true }
"join_rules_state" -> getJoinRules(eventData).run { true } "join_rules_state" -> getJoinRules(eventData).run { true }
"membership_state" -> getMembershipState(eventData).run { true } "membership_state" -> getMembershipState(eventData).run { true }
@ -82,7 +91,7 @@ class WidgetPostAPIHandler(private val context: Context,
// Add "type_" as a prefix // Add "type_" as a prefix
integType?.let { integType = "type_$integType" } 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 val userId = eventData["user_id"] as String
Timber.d("membership_state of $userId in room $roomId requested") 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)) 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" * Update the 'plumbing state"
* *
@ -207,6 +242,7 @@ class WidgetPostAPIHandler(private val context: Context,
* *
* @param eventData the modular data * @param eventData the modular data
*/ */
@Suppress("UNCHECKED_CAST")
private fun setBotOptions(eventData: JsonDict) { private fun setBotOptions(eventData: JsonDict) {
if (checkRoomId(eventData) || checkUserId(eventData)) { if (checkRoomId(eventData) || checkUserId(eventData)) {
return return

View file

@ -20,6 +20,7 @@ import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomWidgetAction : VectorViewModelAction { sealed class RoomWidgetAction : VectorViewModelAction {
data class OnWebViewStartedToLoad(val url: String) : RoomWidgetAction() 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() data class OnWebViewLoadingSuccess(val url: String) : RoomWidgetAction()
object OnTermsReviewed: RoomWidgetAction()
} }

View file

@ -16,16 +16,24 @@
package im.vector.riotx.features.widgets.room package im.vector.riotx.features.widgets.room
import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.View 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.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.util.JsonDict
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment 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.webview.WebViewEventListener
import im.vector.riotx.features.widgets.webview.clearAfterWidget import im.vector.riotx.features.widgets.webview.clearAfterWidget
import im.vector.riotx.features.widgets.webview.setupForWidget import im.vector.riotx.features.widgets.webview.setupForWidget
@ -39,12 +47,13 @@ data class WidgetArgs(
val baseUrl: String, val baseUrl: String,
val kind: WidgetKind, val kind: WidgetKind,
val roomId: String, val roomId: String,
val widgetId: String? = null val widgetId: String? = null,
val urlParams: Map<String, String> = emptyMap()
) : Parcelable ) : Parcelable
class RoomWidgetFragment @Inject constructor( class RoomWidgetFragment @Inject constructor(
private val viewModelFactory: RoomWidgetViewModel.Factory 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 fragmentArgs: WidgetArgs by args()
private val viewModel: RoomWidgetViewModel by fragmentViewModel() private val viewModel: RoomWidgetViewModel by fragmentViewModel()
@ -54,12 +63,31 @@ class RoomWidgetFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
widgetWebView.setupForWidget(this) 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() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
viewModel.getPostAPIMediator().clear() if (fragmentArgs.kind.isAdmin()) {
viewModel.getPostAPIMediator().clearWebView()
}
widgetWebView.clearAfterWidget() widgetWebView.clearAfterWidget()
} }
@ -80,21 +108,99 @@ class RoomWidgetFragment @Inject constructor(
} }
override fun invalidate() = withState(viewModel) { state -> 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) { override fun onPageStarted(url: String) {
viewModel.handle(RoomWidgetAction.OnWebViewStartedToLoad(url))
} }
override fun onPageFinished(url: String) { override fun onPageFinished(url: String) {
viewModel.handle(RoomWidgetAction.OnWebViewLoadingSuccess(url))
} }
override fun onPageError(url: String, errorCode: Int, description: String) { override fun onPageError(url: String, errorCode: Int, description: String) {
viewModel.handle(RoomWidgetAction.OnWebViewLoadingError(url, false, errorCode, description))
} }
override fun handleWidgetRequest(eventData: JsonDict): Boolean { override fun onHttpError(url: String, errorCode: Int, description: String) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 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 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 com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session 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.core.platform.VectorViewModel
import im.vector.riotx.features.widgets.WidgetPostAPIHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.net.ssl.HttpsURLConnection
class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState: WidgetViewState, class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState: WidgetViewState,
private val widgetPostAPIHandlerFactory: WidgetPostAPIHandler.Factory,
private val session: Session) private val session: Session)
: VectorViewModel<WidgetViewState, RoomWidgetAction, RoomWidgetViewEvents>(initialState) { : VectorViewModel<WidgetViewState, RoomWidgetAction, RoomWidgetViewEvents>(initialState) {
@ -54,76 +59,119 @@ class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState
private val widgetService = session.widgetService() private val widgetService = session.widgetService()
private val integrationManagerService = session.integrationManagerService() private val integrationManagerService = session.integrationManagerService()
private val widgetBuilder = widgetService.getWidgetURLBuilder() private val widgetBuilder = widgetService.getWidgetURLFormatter()
private val postAPIMediator = widgetService.getWidgetPostAPIMediator() private val postAPIMediator = widgetService.getWidgetPostAPIMediator()
init { init {
if(initialState.widgetKind.isAdmin()) {
val widgetPostAPIHandler = widgetPostAPIHandlerFactory.create(initialState.roomId)
postAPIMediator.setHandler(widgetPostAPIHandler)
}
refreshPermissionStatus() refreshPermissionStatus()
observePermissionStatus()
}
private fun observePermissionStatus() {
selectSubscribe(WidgetViewState::status) {
Timber.v("Widget status: $it")
if (it == WidgetStatus.WIDGET_ALLOWED) {
loadFormattedUrl()
}
}
} }
fun getPostAPIMediator() = postAPIMediator fun getPostAPIMediator() = postAPIMediator
override fun handle(action: RoomWidgetAction) { override fun handle(action: RoomWidgetAction) {
when (action) { 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.OnWebViewLoadingSuccess -> handleWebViewLoadingSuccess(action.url)
is RoomWidgetAction.OnWebViewStartedToLoad -> handleWebViewStartLoading(action.url) is RoomWidgetAction.OnWebViewStartedToLoad -> handleWebViewStartLoading()
} }
} }
private fun refreshPermissionStatus() { private fun refreshPermissionStatus() {
if (initialState.widgetKind == WidgetKind.USER || initialState.widgetKind == WidgetKind.INTEGRATION_MANAGER) { if (initialState.widgetKind.isAdmin()) {
onWidgetAllowed() setWidgetStatus(WidgetStatus.WIDGET_ALLOWED)
} else { } else {
val widgetId = initialState.widgetId val widgetId = initialState.widgetId
if (widgetId == null) { if (widgetId == null) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) } setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED)
return return
} }
val roomWidget = widgetService.getRoomWidgets(initialState.roomId, widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.SENSITIVE)).firstOrNull() val roomWidget = widgetService.getRoomWidgets(initialState.roomId, widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.SENSITIVE)).firstOrNull()
if (roomWidget == null) { if (roomWidget == null) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) } setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED)
return return
} }
if (roomWidget.event?.senderId == session.myUserId) { if (roomWidget.event?.senderId == session.myUserId) {
onWidgetAllowed() setWidgetStatus(WidgetStatus.WIDGET_ALLOWED)
} else { } else {
val stateEventId = roomWidget.event?.eventId val stateEventId = roomWidget.event?.eventId
// This should not happen // This should not happen
if (stateEventId == null) { if (stateEventId == null) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) } setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED)
return return
} }
val isAllowed = integrationManagerService.isWidgetAllowed(stateEventId) val isAllowed = integrationManagerService.isWidgetAllowed(stateEventId)
if (!isAllowed) { if (!isAllowed) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) } setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED)
} else { } else {
onWidgetAllowed() setWidgetStatus(WidgetStatus.WIDGET_ALLOWED)
} }
} }
} }
} }
private fun onWidgetAllowed() { private fun setWidgetStatus(widgetStatus: WidgetStatus) {
setState { setState { copy(status = widgetStatus) }
copy(status = WidgetStatus.WIDGET_ALLOWED, formattedURL = Loading()) }
}
private fun loadFormattedUrl(forceFetchToken: Boolean = false) {
viewModelScope.launch { viewModelScope.launch {
try { 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)) } setState { copy(formattedURL = Success(formattedUrl)) }
_viewEvents.post(RoomWidgetViewEvents.LoadFormattedURL(formattedUrl))
} catch (failure: Throwable) { } catch (failure: Throwable) {
if (failure is WidgetManagementFailure.TermsNotSignedException) {
_viewEvents.post(RoomWidgetViewEvents.DisplayTerms(failure.baseUrl, failure.token))
}
setState { copy(formattedURL = Fail(failure)) } setState { copy(formattedURL = Fail(failure)) }
} }
} }
} }
private fun handleWebViewStartLoading(url: String) { private fun handleWebViewStartLoading() {
setState { copy(webviewLoadedUrl = Loading()) }
} }
private fun handleWebViewLoadingSuccess(url: String) { 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 { enum class WidgetKind {
ROOM, ROOM,
USER, USER,
INTEGRATION_MANAGER INTEGRATION_MANAGER;
fun isAdmin(): Boolean {
return this == USER || this == INTEGRATION_MANAGER
}
} }
data class WidgetViewState( data class WidgetViewState(
val roomId: String, val roomId: String,
val baseUrl: String, val baseUrl: String,
val urlParams: Map<String, String> = emptyMap(),
val widgetId: String? = null, val widgetId: String? = null,
val widgetKind: WidgetKind, val widgetKind: WidgetKind,
val status: WidgetStatus = WidgetStatus.UNKNOWN, val status: WidgetStatus = WidgetStatus.UNKNOWN,
@ -49,6 +54,7 @@ data class WidgetViewState(
widgetKind = widgetArgs.kind, widgetKind = widgetArgs.kind,
baseUrl = widgetArgs.baseUrl, baseUrl = widgetArgs.baseUrl,
roomId = widgetArgs.roomId, 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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> 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 <item
android:id="@+id/resend_all" android:id="@+id/resend_all"
android:icon="@drawable/ic_refresh_cw" android:icon="@drawable/ic_refresh_cw"