Try to communicate with WidgetPostAPI

This commit is contained in:
ganfra 2020-05-13 20:04:08 +02:00
parent 01d6b52a60
commit 91301197ea
14 changed files with 516 additions and 9 deletions

View file

@ -1,6 +1,9 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="160" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>

View file

@ -0,0 +1,54 @@
var android_widget_events = {};
var sendObjectMessageToRiotAndroid = function(parameters) {
Android.onWidgetEvent(JSON.stringify(parameters));
};
var onWidgetMessageToRiotAndroid = function(event) {
/* Use an internal "_id" field for matching onMessage events and requests
_id was originally used by the Modular API. Keep it */
if (!event.data._id) {
/* The Matrix Widget API v2 spec says:
"The requestId field should be unique and included in all requests" */
event.data._id = event.data.requestId;
}
/* Make sure to have one id */
if (!event.data._id) {
event.data._id = Date.now() + "-" + Math.random().toString(36);
}
console.log("onWidgetMessageToRiotAndroid " + event.data._id);
if (android_widget_events[event.data._id]) {
console.log("onWidgetMessageToRiotAndroid : already managed");
return;
}
if (!event.origin) {
event.origin = event.originalEvent.origin;
}
android_widget_events[event.data._id] = event;
console.log("onWidgetMessageToRiotAndroid : manage " + event.data);
sendObjectMessageToRiotAndroid({'event.data': event.data});
};
var sendResponseFromRiotAndroid = function(eventId, res) {
var event = android_widget_events[eventId];
console.log("sendResponseFromRiotAndroid to " + event.data.action + " for "+ eventId + ": " + JSON.stringify(res));
var data = JSON.parse(JSON.stringify(event.data));
data.response = res;
console.log("sendResponseFromRiotAndroid ---> " + data);
event.source.postMessage(data, event.origin);
android_widget_events[eventId] = true;
console.log("sendResponseFromRiotAndroid to done");
};
window.addEventListener('message', onWidgetMessageToRiotAndroid, false);

View file

@ -40,6 +40,7 @@ import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.widgets.WidgetService
/**
* This interface defines interactions with a session.
@ -61,7 +62,8 @@ interface Session :
HomeServerCapabilitiesService,
SecureStorageService,
AccountDataService,
AccountService {
AccountService,
WidgetService {
/**
* The params associated to the session

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.widgets
import android.webkit.WebView
import im.vector.matrix.android.api.util.JsonDict
interface WidgetPostAPIMediator {
/**
* This initialize the mediator and configure the webview.
* It will add a JavaScript Interface.
* Please call [clear] method when finished to clean the provided webview
*/
fun initialize(webView: WebView, handler: Handler)
/**
* This clear the mediator by removing the JavaScript Interface and cleaning references.
*/
fun clear()
/**
* Inject the necessary javascript into the configured WebView.
* Should be called after a web page has been loaded.
*/
fun injectAPI()
/**
* Send a boolean response
*
* @param response the response
* @param eventData the modular data
*/
fun sendBoolResponse(response: Boolean, eventData: JsonDict)
/**
* Send an integer response
*
* @param response the response
* @param eventData the modular data
*/
fun sendIntegerResponse(response: Int, eventData: JsonDict)
/**
* Send an object response
*
* @param response the response
* @param eventData the modular data
*/
fun sendObjectResponse(response: JsonDict?, eventData: JsonDict)
/**
* Send success
*
* @param eventData the modular data
*/
fun sendSuccess(eventData: JsonDict)
/**
* Send an error
*
* @param message the error message
* @param eventData the modular data
*/
fun sendError(message: String, eventData: JsonDict)
interface Handler {
/**
* Triggered when a widget is posting
*/
fun handleWidgetRequest(data: JsonDict): Boolean
}
}

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.matrix.android.api.session.widgets
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.session.widgets.Widget
interface WidgetService {
fun getWidgetPostAPIMediator(): WidgetPostAPIMediator
fun getRoomWidgets(
roomId: String,
widgetId: QueryStringValue = QueryStringValue.NoCondition,
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): List<Widget>
fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback<Widget>): Cancelable
fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback<Unit>): Cancelable
fun hasPermissionsToHandleWidgets(roomId: String): Boolean
}

View file

@ -29,4 +29,28 @@ data class WidgetContent(
@Json(name = "name") val name: String? = null,
@Json(name = "data") val data: JsonDict = emptyMap(),
@Json(name = "waitForIframeLoad") val waitForIframeLoad: Boolean = false
)
) {
/**
* @return the human name
*/
fun getHumanName(): String {
return if (!name.isNullOrBlank()) {
"$name widget"
} else if (!type.isNullOrBlank()) {
when {
type.contains("widget") -> {
type
}
id != null -> {
"$type $id"
}
else -> {
"$type widget"
}
}
} else {
"Widget $id"
}
}
}

View file

@ -44,6 +44,7 @@ import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.widgets.WidgetService
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater
@ -88,6 +89,7 @@ internal class DefaultSession @Inject constructor(
private val fileService: Lazy<FileService>,
private val secureStorageService: Lazy<SecureStorageService>,
private val profileService: Lazy<ProfileService>,
private val widgetService: Lazy<WidgetService>,
private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver,
private val syncTokenStore: SyncTokenStore,
@ -117,7 +119,8 @@ internal class DefaultSession @Inject constructor(
HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
ProfileService by profileService.get(),
AccountDataService by accountDataService.get(),
AccountService by accountService.get() {
AccountService by accountService.get(),
WidgetService by widgetService.get() {
override val sharedSecretStorageService: SharedSecretStorageService
get() = _sharedSecretStorageService.get()

View file

@ -0,0 +1,173 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.widgets
import android.os.Build
import android.webkit.JavascriptInterface
import android.webkit.WebView
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 timber.log.Timber
import java.util.HashMap
import javax.inject.Inject
internal class DefaultWidgetPostAPIMediator @Inject constructor(moshi: Moshi,
private val widgetPostMessageAPIProvider: WidgetPostMessageAPIProvider) : WidgetPostAPIMediator {
private val adapter = moshi.adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
private var handler: WidgetPostAPIMediator.Handler? = null
private var webView: WebView? = null
override fun initialize(webView: WebView, handler: WidgetPostAPIMediator.Handler) {
this.webView = webView
this.handler = handler
webView.addJavascriptInterface(this, "WidgetPostAPIMediator")
}
override fun clear() {
handler = null
webView?.removeJavascriptInterface("WidgetPostAPIMediator")
webView = null
}
override fun injectAPI() {
val js = widgetPostMessageAPIProvider.get()
if (null != js) {
webView?.loadUrl("javascript:$js")
}
}
@JavascriptInterface
fun onWidgetEvent(jsonEventData: String) {
Timber.d("BRIDGE onWidgetEvent : $jsonEventData")
try {
val dataAsDict = adapter.fromJson(jsonEventData)
@Suppress("UNCHECKED_CAST")
val eventData = (dataAsDict?.get("event.data") as? JsonDict) ?: return
onWidgetMessage(eventData)
} catch (e: Exception) {
Timber.e(e, "## onWidgetEvent() failed")
}
}
private fun onWidgetMessage(eventData: JsonDict) {
try {
if (handler?.handleWidgetRequest(eventData) == false) {
sendError("", eventData)
}
} catch (e: Exception) {
Timber.e(e, "## onWidgetMessage() : failed")
sendError("", eventData)
}
}
/*
* *********************************************************************************************
* Message sending methods
* *********************************************************************************************
*/
/**
* Send a boolean response
*
* @param response the response
* @param eventData the modular data
*/
override fun sendBoolResponse(response: Boolean, eventData: JsonDict) {
val jsString = if (response) "true" else "false"
sendResponse(jsString, eventData)
}
/**
* Send an integer response
*
* @param response the response
* @param eventData the modular data
*/
override fun sendIntegerResponse(response: Int, eventData: JsonDict) {
sendResponse(response.toString() + "", eventData)
}
/**
* Send an object response
*
* @param response the response
* @param eventData the modular data
*/
override fun sendObjectResponse(response: JsonDict?, eventData: JsonDict) {
var jsString: String? = null
if (response != null) {
try {
jsString = "JSON.parse('${adapter.toJson(response)}')"
} catch (e: Exception) {
Timber.e(e, "## sendObjectResponse() : toJson failed ")
}
}
sendResponse(jsString ?: "null", eventData)
}
/**
* Send success
*
* @param eventData the modular data
*/
override fun sendSuccess(eventData: JsonDict) {
val successResponse = mapOf("success" to true)
sendObjectResponse(successResponse, eventData)
}
/**
* Send an error
*
* @param message the error message
* @param eventData the modular data
*/
override fun sendError(message: String, eventData: JsonDict) {
Timber.e("## sendError() : eventData $eventData failed $message")
// TODO: JS has an additional optional parameter: nestedError
val params = HashMap<String, Map<String, String>>()
val subMap = HashMap<String, String>()
subMap["message"] = message
params["error"] = subMap
sendObjectResponse(params, eventData)
}
/**
* Send the response to the javascript
*
* @param jsString the response data
* @param eventData the modular data
*/
private fun sendResponse(jsString: String, eventData: JsonDict) {
try {
val functionLine = "sendResponseFromRiotAndroid('" + eventData["_id"] + "' , " + jsString + ");"
Timber.v("BRIDGE sendResponse: $functionLine")
// call the javascript method
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
webView?.loadUrl("javascript:$functionLine")
} else {
webView?.evaluateJavascript(functionLine, null)
}
} catch (e: Exception) {
Timber.e(e, "## sendResponse() failed ")
}
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.widgets
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator
import im.vector.matrix.android.api.session.widgets.WidgetService
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 widgetPostAPIMediator: Provider<WidgetPostAPIMediator>) : WidgetService {
override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator {
return widgetPostAPIMediator.get()
}
override fun getRoomWidgets(roomId: String, widgetId: QueryStringValue, widgetTypes: Set<String>?, excludedTypes: Set<String>?): List<Widget> {
return widgetManager.getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
}
override fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback<Widget>): Cancelable {
return widgetManager.createWidget(roomId, widgetId, content, callback)
}
override fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback<Unit>): Cancelable {
return widgetManager.destroyWidget(roomId, widgetId, callback)
}
override fun hasPermissionsToHandleWidgets(roomId: String): Boolean {
return widgetManager.hasPermissionsToHandleWidgets(roomId)
}
}

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.session.widgets
import im.vector.matrix.android.api.failure.Failure
sealed class CreateWidgetFailure : Failure.FeatureFailure() {
object NotEnoughtPower : CreateWidgetFailure()
object CreationFailed : CreateWidgetFailure()
sealed class WidgetManagementFailure : Failure.FeatureFailure() {
object NotEnoughPower : WidgetManagementFailure()
object CreationFailed : WidgetManagementFailure()
}

View file

@ -101,7 +101,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
fun createWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback<Widget>): Cancelable {
return taskExecutor.executorScope.launchToCallback(callback = callback) {
if (!hasPermissionsToHandleWidgets(roomId)) {
throw CreateWidgetFailure.NotEnoughtPower
throw WidgetManagementFailure.NotEnoughPower
}
val params = CreateWidgetTask.Params(
roomId = roomId,
@ -112,11 +112,25 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
try {
getRoomWidgets(roomId, widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.INSENSITIVE)).first()
} catch (failure: Throwable) {
throw CreateWidgetFailure.CreationFailed
throw WidgetManagementFailure.CreationFailed
}
}
}
fun destroyWidget(roomId: String, widgetId: String, callback: MatrixCallback<Unit>): Cancelable {
return taskExecutor.executorScope.launchToCallback(callback = callback) {
if (!hasPermissionsToHandleWidgets(roomId)) {
throw WidgetManagementFailure.NotEnoughPower
}
val params = CreateWidgetTask.Params(
roomId = roomId,
widgetId = widgetId,
content = emptyMap()
)
createWidgetTask.execute(params)
}
}
fun hasPermissionsToHandleWidgets(roomId: String): Boolean {
val powerLevelsEvent = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false

View file

@ -19,6 +19,8 @@ package im.vector.matrix.android.internal.session.widgets
import dagger.Binds
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.internal.session.widgets.token.DefaultGetScalarTokenTask
import im.vector.matrix.android.internal.session.widgets.token.GetScalarTokenTask
import retrofit2.Retrofit
@ -35,6 +37,12 @@ internal abstract class WidgetModule {
}
}
@Binds
abstract fun bindWidgetService(widgetService: DefaultWidgetService): WidgetService
@Binds
abstract fun bindWidgetPostAPIMediator(widgetPostMessageAPIProvider: DefaultWidgetPostAPIMediator): WidgetPostAPIMediator
@Binds
abstract fun bindCreateWidgetTask(task: DefaultCreateWidgetTask): CreateWidgetTask

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.widgets
import android.content.Context
import timber.log.Timber
import javax.inject.Inject
class WidgetPostMessageAPIProvider @Inject constructor(private val context: Context) {
private var postMessageAPIString: String? = null
fun get(): String? {
if (postMessageAPIString == null) {
postMessageAPIString = readFromAsset(context)
}
return postMessageAPIString
}
private fun readFromAsset(context: Context): String? {
return try {
context.assets.open("postMessageAPI.js").bufferedReader().use {
it.readText()
}
} catch (failure: Throwable) {
Timber.e(failure, "Reading postMessageAPI.js asset failed")
null
}
}
}

View file

@ -22,7 +22,9 @@ import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.toCancelable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@ -34,5 +36,7 @@ internal fun <T> CoroutineScope.launchToCallback(
val result = runCatching {
block()
}
result.foldToCallback(callback)
withContext(Dispatchers.Main) {
result.foldToCallback(callback)
}
}.toCancelable()