mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Widget: makes the integration manager screen mostly working
This commit is contained in:
parent
00fd067c6b
commit
df973a6275
34 changed files with 495 additions and 160 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -25,7 +25,7 @@ import im.vector.matrix.android.internal.session.widgets.Widget
|
|||
|
||||
interface WidgetService {
|
||||
|
||||
fun getWidgetURLBuilder(): WidgetURLBuilder
|
||||
fun getWidgetURLFormatter(): WidgetURLFormatter
|
||||
|
||||
fun getWidgetPostAPIMediator(): WidgetPostAPIMediator
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
))
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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)) {
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -162,6 +162,7 @@
|
|||
android:theme="@style/AppTheme.AttachmentsPreview" />
|
||||
|
||||
<activity android:name=".features.terms.ReviewTermsActivity" />
|
||||
<activity android:name=".features.widgets.WidgetActivity" />
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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?)
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue