feat(plugins): typed invoke arguments for mobile plugins (#8076)

This commit is contained in:
Lucas Fernandes Nogueira
2023-10-23 14:09:42 -03:00
committed by GitHub
parent a74ff464bb
commit 198abe3c2c
31 changed files with 434 additions and 937 deletions

View File

@@ -0,0 +1,5 @@
---
"tauri": patch:breaking
---
The Android `PluginManager.loadConfig` now takes a third parameter to define the class type of the config object.

View File

@@ -0,0 +1,5 @@
---
"tauri": patch:enhance
---
Mobile plugins can now resolve using an arbitrary object instead of using the `JSObject` class via `Invoke.resolve` on iOS and `Invoke.resolveObject` on Android.

View File

@@ -0,0 +1,5 @@
---
"tauri": patch:breaking
---
Mobile plugins now have access to a parser for the invoke arguments instead of relying on the `Invoke#get${TYPE}` methods.

View File

@@ -0,0 +1,6 @@
---
"tauri-cli": patch:breaking
"@tauri-apps/cli": patch:breaking
---
Updated the mobile plugin templates following the tauri v2.0.0-alpha.17 changes.

View File

@@ -42,7 +42,7 @@ development = [ "quickcheck_macros" ]
[dependencies]
serde_json = { version = "1.0", features = [ "raw_value" ] }
serde = { version = "1.0", features = [ "derive" ] }
serde = { version = "1.0", features = [ "derive", "rc" ] }
tokio = { version = "1", features = [ "rt", "rt-multi-thread", "sync", "fs", "io-util" ] }
futures-util = "0.3"
uuid = { version = "1", features = [ "v4" ], optional = true }

View File

@@ -38,6 +38,7 @@ dependencies {
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.7.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

View File

@@ -23,3 +23,7 @@
@app.tauri.annotation.Permission <methods>;
public <init>(...);
}
-keep @app.tauri.annotation.InvokeArg public class * {
*;
}

View File

@@ -95,10 +95,10 @@ object PermissionHelper {
* @param neededPermissions The permissions needed.
* @return The permissions not present in AndroidManifest.xml
*/
fun getUndefinedPermissions(context: Context, neededPermissions: Array<String>): Array<String?> {
val undefinedPermissions = ArrayList<String?>()
fun getUndefinedPermissions(context: Context, neededPermissions: Array<String>): Array<String> {
val undefinedPermissions = ArrayList<String>()
val requestedPermissions = getManifestPermissions(context)
if (requestedPermissions != null && requestedPermissions.isNotEmpty()) {
if (!requestedPermissions.isNullOrEmpty()) {
val requestedPermissionsList = listOf(*requestedPermissions)
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
for (permission in neededPermissions) {
@@ -106,10 +106,8 @@ object PermissionHelper {
undefinedPermissions.add(permission)
}
}
var undefinedPermissionArray = arrayOfNulls<String>(undefinedPermissions.size)
undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray)
return undefinedPermissionArray
return undefinedPermissions.toTypedArray()
}
return neededPermissions as Array<String?>
return neededPermissions
}
}

View File

@@ -0,0 +1,9 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.annotation
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class InvokeArg

View File

@@ -4,8 +4,30 @@
package app.tauri.plugin
class Channel(val id: Long, private val handler: (data: JSObject) -> Unit) {
fun send(data: JSObject) {
handler(data)
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.ObjectMapper
const val CHANNEL_PREFIX = "__CHANNEL__:"
internal class ChannelDeserializer(val sendChannelData: (channelId: Long, data: String) -> Unit, private val objectMapper: ObjectMapper): JsonDeserializer<Channel>() {
override fun deserialize(
jsonParser: JsonParser?,
deserializationContext: DeserializationContext
): Channel {
val channelDef = deserializationContext.readValue(jsonParser, String::class.java)
val callback = channelDef.substring(CHANNEL_PREFIX.length).toLongOrNull() ?: throw Error("unexpected channel value $channelDef")
return Channel(callback, { res -> sendChannelData(callback, res) }, objectMapper)
}
}
class Channel(val id: Long, private val handler: (data: String) -> Unit, private val objectMapper: ObjectMapper) {
fun send(data: JSObject) {
handler(PluginResult(data).toString())
}
fun sendObject(data: Any) {
handler(objectMapper.writeValueAsString(data))
}
}

View File

@@ -5,42 +5,54 @@
package app.tauri.plugin
import app.tauri.Logger
const val CHANNEL_PREFIX = "__CHANNEL__:"
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
class Invoke(
val id: Long,
val command: String,
val callback: Long,
val error: Long,
private val sendResponse: (callback: Long, data: PluginResult?) -> Unit,
private val sendChannelData: (channelId: Long, data: PluginResult) -> Unit,
val data: JSObject) {
private val sendResponse: (callback: Long, data: String) -> Unit,
private val argsJson: String,
private val jsonMapper: ObjectMapper
) {
fun<T> parseArgs(cls: Class<T>): T {
return jsonMapper.readValue(argsJson, cls)
}
fun<T> parseArgs(ref: TypeReference<T>): T {
return jsonMapper.readValue(argsJson, ref)
}
fun resolve(data: JSObject?) {
val result = PluginResult(data)
sendResponse(callback, result)
sendResponse(callback, PluginResult(data).toString())
}
fun resolveObject(data: Any) {
sendResponse(callback, jsonMapper.writeValueAsString(data))
}
fun resolve() {
sendResponse(callback, null)
sendResponse(callback, "null")
}
fun reject(msg: String?, code: String?, ex: Exception?, data: JSObject?) {
val errorResult = PluginResult()
if (ex != null) {
Logger.error(Logger.tags("Plugin"), msg!!, ex)
}
try {
errorResult.put("message", msg)
errorResult.put("message", msg)
if (code != null) {
errorResult.put("code", code)
if (null != data) {
errorResult.put("data", data)
}
} catch (jsonEx: Exception) {
Logger.error(Logger.tags("Plugin"), jsonEx.message!!, jsonEx)
}
sendResponse(error, errorResult)
if (data != null) {
errorResult.put("data", data)
}
sendResponse(error, errorResult.toString())
}
fun reject(msg: String?, ex: Exception?, data: JSObject?) {
@@ -70,142 +82,4 @@ class Invoke(
fun reject(msg: String?) {
reject(msg, null, null, null)
}
fun getString(name: String): String? {
return getStringInternal(name, null)
}
fun getString(name: String, defaultValue: String): String {
return getStringInternal(name, defaultValue)!!
}
private fun getStringInternal(name: String, defaultValue: String?): String? {
val value = data.opt(name) ?: return defaultValue
return if (value is String) {
value
} else defaultValue
}
fun getInt(name: String): Int? {
return getIntInternal(name, null)
}
fun getInt(name: String, defaultValue: Int): Int {
return getIntInternal(name, defaultValue)!!
}
private fun getIntInternal(name: String, defaultValue: Int?): Int? {
val value = data.opt(name) ?: return defaultValue
return if (value is Int) {
value
} else defaultValue
}
fun getLong(name: String): Long? {
return getLongInternal(name, null)
}
fun getLong(name: String, defaultValue: Long): Long {
return getLongInternal(name, defaultValue)!!
}
private fun getLongInternal(name: String, defaultValue: Long?): Long? {
val value = data.opt(name) ?: return defaultValue
return if (value is Long) {
value
} else defaultValue
}
fun getFloat(name: String): Float? {
return getFloatInternal(name, null)
}
fun getFloat(name: String, defaultValue: Float): Float {
return getFloatInternal(name, defaultValue)!!
}
private fun getFloatInternal(name: String, defaultValue: Float?): Float? {
val value = data.opt(name) ?: return defaultValue
if (value is Float) {
return value
}
if (value is Double) {
return value.toFloat()
}
return if (value is Int) {
value.toFloat()
} else defaultValue
}
fun getDouble(name: String): Double? {
return getDoubleInternal(name, null)
}
fun getDouble(name: String, defaultValue: Double): Double {
return getDoubleInternal(name, defaultValue)!!
}
private fun getDoubleInternal(name: String, defaultValue: Double?): Double? {
val value = data.opt(name) ?: return defaultValue
if (value is Double) {
return value
}
if (value is Float) {
return value.toDouble()
}
return if (value is Int) {
value.toDouble()
} else defaultValue
}
fun getBoolean(name: String): Boolean? {
return getBooleanInternal(name, null)
}
fun getBoolean(name: String, defaultValue: Boolean): Boolean {
return getBooleanInternal(name, defaultValue)!!
}
private fun getBooleanInternal(name: String, defaultValue: Boolean?): Boolean? {
val value = data.opt(name) ?: return defaultValue
return if (value is Boolean) {
value
} else defaultValue
}
fun getObject(name: String): JSObject? {
return getObjectInternal(name, null)
}
fun getObject(name: String, defaultValue: JSObject): JSObject {
return getObjectInternal(name, defaultValue)!!
}
private fun getObjectInternal(name: String, defaultValue: JSObject?): JSObject? {
val value = data.opt(name) ?: return defaultValue
return if (value is JSObject) value else defaultValue
}
fun getArray(name: String): JSArray? {
return getArrayInternal(name, null)
}
fun getArray(name: String, defaultValue: JSArray): JSArray {
return getArrayInternal(name, defaultValue)!!
}
private fun getArrayInternal(name: String, defaultValue: JSArray?): JSArray? {
val value = data.opt(name) ?: return defaultValue
return if (value is JSArray) value else defaultValue
}
fun hasOption(name: String): Boolean {
return data.has(name)
}
fun getChannel(name: String): Channel? {
val channelDef = getString(name, "")
val callback = channelDef.substring(CHANNEL_PREFIX.length).toLongOrNull() ?: return null
return Channel(callback) { res -> sendChannelData(callback, PluginResult(res)) }
}
}

View File

@@ -16,20 +16,42 @@ import app.tauri.PermissionHelper
import app.tauri.PermissionState
import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.TauriPlugin
import com.fasterxml.jackson.databind.ObjectMapper
import org.json.JSONException
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
@InvokeArg
internal class RegisterListenerArgs {
lateinit var event: String
lateinit var handler: Channel
}
@InvokeArg
internal class RemoveListenerArgs {
lateinit var event: String
var channelId: Long = 0
}
@InvokeArg internal class RequestPermissionsArgs {
var permissions: List<String>? = null
}
abstract class Plugin(private val activity: Activity) {
var handle: PluginHandle? = null
private val listeners: MutableMap<String, MutableList<Channel>> = mutableMapOf()
open fun load(webView: WebView) {}
fun getConfig(): JSObject {
return handle!!.config
fun jsonMapper(): ObjectMapper {
return handle!!.jsonMapper
}
fun<T> getConfig(cls: Class<T>): T {
return jsonMapper().readValue(handle!!.config, cls)
}
/**
@@ -88,20 +110,25 @@ abstract class Plugin(private val activity: Activity) {
}
}
fun triggerObject(event: String, payload: Any) {
val eventListeners = listeners[event]
if (!eventListeners.isNullOrEmpty()) {
val listeners = CopyOnWriteArrayList(eventListeners)
for (channel in listeners) {
channel.sendObject(payload)
}
}
}
@Command
open fun registerListener(invoke: Invoke) {
val event = invoke.getString("event")
val channel = invoke.getChannel("handler")
val args = invoke.parseArgs(RegisterListenerArgs::class.java)
if (event == null || channel == null) {
invoke.reject("`event` or `handler` not provided")
val eventListeners = listeners[args.event]
if (eventListeners.isNullOrEmpty()) {
listeners[args.event] = mutableListOf(args.handler)
} else {
val eventListeners = listeners[event]
if (eventListeners.isNullOrEmpty()) {
listeners[event] = mutableListOf(channel)
} else {
eventListeners.add(channel)
}
eventListeners.add(args.handler)
}
invoke.resolve()
@@ -109,18 +136,13 @@ abstract class Plugin(private val activity: Activity) {
@Command
open fun removeListener(invoke: Invoke) {
val event = invoke.getString("event")
val channelId = invoke.getLong("channelId")
val args = invoke.parseArgs(RemoveListenerArgs::class.java)
if (event == null || channelId == null) {
invoke.reject("`event` or `channelId` not provided")
} else {
val eventListeners = listeners[event]
if (!eventListeners.isNullOrEmpty()) {
val c = eventListeners.find { c -> c.id == channelId }
if (c != null) {
eventListeners.remove(c)
}
val eventListeners = listeners[args.event]
if (!eventListeners.isNullOrEmpty()) {
val c = eventListeners.find { c -> c.id == args.channelId }
if (c != null) {
eventListeners.remove(c)
}
}
@@ -165,26 +187,32 @@ abstract class Plugin(private val activity: Activity) {
var permAliases: Array<String>? = null
val autoGrantPerms: MutableSet<String> = HashSet()
// If call was made with a list of specific permission aliases to request, save them
// to be requested
val providedPerms: JSArray = invoke.getArray("permissions", JSArray())
var providedPermsList: List<String?>? = null
try {
providedPermsList = providedPerms.toList()
} catch (ignore: JSONException) {
// do nothing
}
val args = invoke.parseArgs(RequestPermissionsArgs::class.java)
args.permissions?.let {
val aliasSet: MutableSet<String> = HashSet()
for (perm in annotation.permissions) {
if (it.contains(perm.alias)) {
aliasSet.add(perm.alias)
}
}
if (aliasSet.isEmpty()) {
invoke.reject("No valid permission alias was requested of this plugin.")
return
} else {
permAliases = aliasSet.toTypedArray()
}
} ?: run {
val aliasSet: MutableSet<String> = HashSet()
// If call was made without any custom permissions, request all from plugin annotation
val aliasSet: MutableSet<String> = HashSet()
if (providedPermsList.isNullOrEmpty()) {
for (perm in annotation.permissions) {
// If a permission is defined with no permission strings, separate it for auto-granting.
// Otherwise, the alias is added to the list to be requested.
if (perm.strings.isEmpty() || perm.strings.size == 1 && perm.strings[0]
.isEmpty()
) {
if (!perm.alias.isEmpty()) {
if (perm.alias.isNotEmpty()) {
autoGrantPerms.add(perm.alias)
}
} else {
@@ -192,31 +220,23 @@ abstract class Plugin(private val activity: Activity) {
}
}
permAliases = aliasSet.toTypedArray()
} else {
for (perm in annotation.permissions) {
if (providedPermsList.contains(perm.alias)) {
aliasSet.add(perm.alias)
}
}
if (aliasSet.isEmpty()) {
invoke.reject("No valid permission alias was requested of this plugin.")
} else {
permAliases = aliasSet.toTypedArray()
}
}
if (!permAliases.isNullOrEmpty()) {
permAliases?.let {
// request permissions using provided aliases or all defined on the plugin
requestPermissionForAliases(permAliases, invoke, "checkPermissions")
} else if (autoGrantPerms.isNotEmpty()) {
// if the plugin only has auto-grant permissions, return all as GRANTED
val permissionsResults = JSObject()
for (perm in autoGrantPerms) {
permissionsResults.put(perm, PermissionState.GRANTED.toString())
requestPermissionForAliases(it, invoke, "checkPermissions")
} ?: run {
if (autoGrantPerms.isNotEmpty()) {
// if the plugin only has auto-grant permissions, return all as GRANTED
val permissionsResults = JSObject()
for (perm in autoGrantPerms) {
permissionsResults.put(perm, PermissionState.GRANTED.toString())
}
invoke.resolve(permissionsResults)
} else {
// no permissions are defined on the plugin, resolve undefined
invoke.resolve()
}
invoke.resolve(permissionsResults)
} else {
// no permissions are defined on the plugin, resolve undefined
invoke.resolve()
}
}
}
@@ -288,15 +308,15 @@ abstract class Plugin(private val activity: Activity) {
* [PermissionCallback] annotation.
*
* @param alias an alias defined on the plugin
* @param invoke the invoke involved in originating the request
* @param invoke the invoke involved in originating the request
* @param callbackName the name of the callback to run when the permission request is complete
*/
protected fun requestPermissionForAlias(
alias: String,
call: Invoke,
invoke: Invoke,
callbackName: String
) {
requestPermissionForAliases(arrayOf(alias), call, callbackName)
requestPermissionForAliases(arrayOf(alias), invoke, callbackName)
}
/**

View File

@@ -15,11 +15,10 @@ import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.Command
import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.TauriPlugin
import com.fasterxml.jackson.databind.ObjectMapper
import java.lang.reflect.Method
import java.util.Arrays
class PluginHandle(private val manager: PluginManager, val name: String, val instance: Plugin, val config: JSObject) {
class PluginHandle(private val manager: PluginManager, val name: String, val instance: Plugin, val config: String, val jsonMapper: ObjectMapper) {
private val commands: HashMap<String, CommandData> = HashMap()
private val permissionCallbackMethods: HashMap<String, Method> = HashMap()
private val startActivityCallbackMethods: HashMap<String, Method> = HashMap()

View File

@@ -11,9 +11,15 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import app.tauri.annotation.InvokeArg
import app.tauri.FsUtils
import app.tauri.JniMethod
import app.tauri.Logger
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import java.lang.reflect.InvocationTargetException
class PluginManager(val activity: AppCompatActivity) {
fun interface RequestPermissionsCallback {
@@ -29,6 +35,7 @@ class PluginManager(val activity: AppCompatActivity) {
private val requestPermissionsLauncher: ActivityResultLauncher<Array<String>>
private var requestPermissionsCallback: RequestPermissionsCallback? = null
private var startActivityForResultCallback: ActivityResultCallback? = null
private var jsonMapper: ObjectMapper
init {
startActivityForResultLauncher =
@@ -46,6 +53,16 @@ class PluginManager(val activity: AppCompatActivity) {
requestPermissionsCallback!!.onResult(result)
}
}
jsonMapper = ObjectMapper()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
val channelDeserializer = ChannelDeserializer({ channelId, payload ->
sendChannelData(channelId, payload)
}, jsonMapper)
jsonMapper
.registerModule(SimpleModule().addDeserializer(Channel::class.java, channelDeserializer))
}
fun onNewIntent(intent: Intent) {
@@ -77,8 +94,8 @@ class PluginManager(val activity: AppCompatActivity) {
}
@JniMethod
fun load(webView: WebView?, name: String, plugin: Plugin, config: JSObject) {
val handle = PluginHandle(this, name, plugin, config)
fun load(webView: WebView?, name: String, plugin: Plugin, config: String) {
val handle = PluginHandle(this, name, plugin, config, jsonMapper)
plugins[name] = handle
if (webView != null) {
plugin.load(webView)
@@ -86,21 +103,19 @@ class PluginManager(val activity: AppCompatActivity) {
}
@JniMethod
fun runCommand(id: Int, pluginId: String, command: String, data: JSObject) {
fun runCommand(id: Int, pluginId: String, command: String, data: String) {
val successId = 0L
val errorId = 1L
val invoke = Invoke(id.toLong(), command, successId, errorId, { fn, result ->
var success: PluginResult? = null
var error: PluginResult? = null
var success: String? = null
var error: String? = null
if (fn == successId) {
success = result
} else {
error = result
}
handlePluginResponse(id, success?.toString(), error?.toString())
}, { channelId, payload ->
sendChannelData(channelId, payload.toString())
}, data)
handlePluginResponse(id, success, error)
}, data, jsonMapper)
dispatchPluginMessage(invoke, pluginId)
}
@@ -119,19 +134,31 @@ class PluginManager(val activity: AppCompatActivity) {
plugins[pluginId]?.invoke(invoke)
}
} catch (e: Exception) {
invoke.reject(if (e.message?.isEmpty() != false) { e.toString() } else { e.message })
var exception: Throwable = e
if (exception.message?.isEmpty() != false) {
if (e is InvocationTargetException) {
exception = e.targetException
}
}
invoke.reject(if (exception.message?.isEmpty() != false) { exception.toString() } else { exception.message })
}
}
companion object {
fun loadConfig(context: Context, plugin: String): JSObject {
fun<T> loadConfig(context: Context, plugin: String, cls: Class<T>): T {
val tauriConfigJson = FsUtils.readAsset(context.assets, "tauri.conf.json")
val tauriConfig = JSObject(tauriConfigJson)
val plugins = tauriConfig.getJSObject("plugins", JSObject())
return plugins.getJSObject(plugin, JSObject())
val mapper = ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
val config = mapper.readValue(tauriConfigJson, Config::class.java)
return mapper.readValue(config.plugins[plugin].toString(), cls)
}
}
private external fun handlePluginResponse(id: Int, success: String?, error: String?)
private external fun sendChannelData(id: Long, data: String)
}
@InvokeArg
internal class Config {
lateinit var plugins: Map<String, JsonNode>
}

View File

@@ -2,16 +2,64 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
public class Channel {
public let id: UInt64
let handler: (JsonValue) -> Void
import Foundation
public init(id: UInt64, handler: @escaping (JsonValue) -> Void) {
self.id = id
let CHANNEL_PREFIX = "__CHANNEL__:"
let channelDataKey = CodingUserInfoKey(rawValue: "sendChannelData")!
public class Channel: Decodable {
public let id: UInt64
let handler: (String) -> Void
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let channelDef = try container.decode(String.self)
let components = channelDef.components(separatedBy: CHANNEL_PREFIX)
if components.count < 2 {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid channel definition from \(channelDef)"
)
}
guard let channelId = UInt64(components[1]) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid channel ID from \(channelDef)"
)
}
guard let handler = decoder.userInfo[channelDataKey] as? (String) -> Void else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "missing userInfo for Channel handler. This is a Tauri issue"
)
}
self.id = channelId
self.handler = handler
}
public func send(_ data: JsonObject) {
handler(.dictionary(data))
func serialize(_ data: JsonValue) -> String {
do {
return try data.jsonRepresentation() ?? "\"Failed to serialize payload\""
} catch {
return "\"\(error)\""
}
}
public func send(_ data: JsonObject) {
send(.dictionary(data))
}
public func send(_ data: JsonValue) {
handler(serialize(data))
}
public func send<T: Encodable>(_ data: T) throws {
let json = try JSONEncoder().encode(data)
handler(String(decoding: json, as: UTF8.self))
}
}

View File

@@ -5,37 +5,42 @@
import Foundation
import UIKit
let CHANNEL_PREFIX = "__CHANNEL__:"
@objc public class Invoke: NSObject, JSValueContainer, BridgedJSValueContainer {
public var dictionaryRepresentation: NSDictionary {
return data as NSDictionary
}
public static var jsDateFormatter: ISO8601DateFormatter = {
return ISO8601DateFormatter()
}()
public var command: String
var callback: UInt64
var error: UInt64
public var data: JSObject
var sendResponse: (UInt64, JsonValue?) -> Void
var sendChannelData: (UInt64, JsonValue) -> Void
@objc public class Invoke: NSObject {
public let command: String
let callback: UInt64
let error: UInt64
let data: String
let sendResponse: (UInt64, String?) -> Void
let sendChannelData: (UInt64, String) -> Void
public init(
command: String, callback: UInt64, error: UInt64,
sendResponse: @escaping (UInt64, JsonValue?) -> Void,
sendChannelData: @escaping (UInt64, JsonValue) -> Void, data: JSObject?
sendResponse: @escaping (UInt64, String?) -> Void,
sendChannelData: @escaping (UInt64, String) -> Void, data: String
) {
self.command = command
self.callback = callback
self.error = error
self.data = data ?? [:]
self.data = data
self.sendResponse = sendResponse
self.sendChannelData = sendChannelData
}
public func parseArgs<T: Decodable>(_ type: T.Type) throws -> T {
let jsonData = self.data.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.userInfo[channelDataKey] = sendChannelData
return try decoder.decode(type, from: jsonData)
}
func serialize(_ data: JsonValue) -> String {
do {
return try data.jsonRepresentation() ?? "\"Failed to serialize payload\""
} catch {
return "\"\(error)\""
}
}
public func resolve() {
sendResponse(callback, nil)
}
@@ -45,15 +50,33 @@ let CHANNEL_PREFIX = "__CHANNEL__:"
}
public func resolve(_ data: JsonValue) {
sendResponse(callback, data)
sendResponse(callback, serialize(data))
}
public func resolve<T: Encodable>(_ data: T) {
do {
let json = try JSONEncoder().encode(data)
sendResponse(callback, String(decoding: json, as: UTF8.self))
} catch {
sendResponse(self.error, "\"\(error)\"")
}
}
public func reject(
_ message: String, _ code: String? = nil, _ error: Error? = nil, _ data: JsonValue? = nil
_ message: String, code: String? = nil, error: Error? = nil, data: JsonValue? = nil
) {
let payload: NSMutableDictionary = [
"message": message, "code": code ?? "", "error": error ?? "",
"message": message
]
if let code = code {
payload["code"] = code
}
if let error = error {
payload["error"] = error
}
if let data = data {
switch data {
case .dictionary(let dict):
@@ -62,7 +85,8 @@ let CHANNEL_PREFIX = "__CHANNEL__:"
}
}
}
sendResponse(self.error, .dictionary(payload as! JsonObject))
sendResponse(self.error, serialize(.dictionary(payload as! JsonObject)))
}
public func unimplemented() {
@@ -70,7 +94,7 @@ let CHANNEL_PREFIX = "__CHANNEL__:"
}
public func unimplemented(_ message: String) {
sendResponse(error, .dictionary(["message": message]))
reject(message)
}
public func unavailable() {
@@ -78,22 +102,6 @@ let CHANNEL_PREFIX = "__CHANNEL__:"
}
public func unavailable(_ message: String) {
sendResponse(error, .dictionary(["message": message]))
}
public func getChannel(_ key: String) -> Channel? {
let channelDef = getString(key, "")
let components = channelDef.components(separatedBy: CHANNEL_PREFIX)
if components.count < 2 {
return nil
}
guard let channelId = UInt64(components[1]) else {
return nil
}
return Channel(
id: channelId,
handler: { (res: JsonValue) -> Void in
self.sendChannelData(channelId, res)
})
reject(message)
}
}

View File

@@ -5,238 +5,18 @@
import Foundation
// declare our empty protocol, and conformance, for typing
public protocol JSValue { }
extension String: JSValue { }
extension Bool: JSValue { }
extension Int: JSValue { }
extension Float: JSValue { }
extension Double: JSValue { }
extension NSNumber: JSValue { }
extension NSNull: JSValue { }
extension Array: JSValue { }
extension Date: JSValue { }
extension Dictionary: JSValue where Key == String, Value == JSValue { }
public protocol JSValue {}
extension String: JSValue {}
extension Bool: JSValue {}
extension Int: JSValue {}
extension Float: JSValue {}
extension Double: JSValue {}
extension NSNumber: JSValue {}
extension NSNull: JSValue {}
extension Array: JSValue {}
extension Date: JSValue {}
extension Dictionary: JSValue where Key == String, Value == JSValue {}
// convenience aliases
public typealias JSObject = [String: JSValue]
public typealias JSArray = [JSValue]
// string types
public protocol JSStringContainer {
func getString(_ key: String, _ defaultValue: String) -> String
func getString(_ key: String) -> String?
}
extension JSStringContainer {
public func getString(_ key: String, _ defaultValue: String) -> String {
return getString(key) ?? defaultValue
}
}
// boolean types
public protocol JSBoolContainer {
func getBool(_ key: String, _ defaultValue: Bool) -> Bool
func getBool(_ key: String) -> Bool?
}
extension JSBoolContainer {
public func getBool(_ key: String, _ defaultValue: Bool) -> Bool {
return getBool(key) ?? defaultValue
}
}
// integer types
public protocol JSIntContainer {
func getInt(_ key: String, _ defaultValue: Int) -> Int
func getInt(_ key: String) -> Int?
}
extension JSIntContainer {
public func getInt(_ key: String, _ defaultValue: Int) -> Int {
return getInt(key) ?? defaultValue
}
}
// float types
public protocol JSFloatContainer {
func getFloat(_ key: String, _ defaultValue: Float) -> Float
func getFloat(_ key: String) -> Float?
}
extension JSFloatContainer {
public func getFloat(_ key: String, _ defaultValue: Float) -> Float {
return getFloat(key) ?? defaultValue
}
}
// double types
public protocol JSDoubleContainer {
func getDouble(_ key: String, _ defaultValue: Double) -> Double
func getDouble(_ key: String) -> Double?
}
extension JSDoubleContainer {
public func getDouble(_ key: String, _ defaultValue: Double) -> Double {
return getDouble(key) ?? defaultValue
}
}
// date types
public protocol JSDateContainer {
func getDate(_ key: String, _ defaultValue: Date) -> Date
func getDate(_ key: String) -> Date?
}
extension JSDateContainer {
public func getDate(_ key: String, _ defaultValue: Date) -> Date {
return getDate(key) ?? defaultValue
}
}
// array types
public protocol JSArrayContainer {
func getArray(_ key: String, _ defaultValue: JSArray) -> JSArray
func getArray<T>(_ key: String, _ ofType: T.Type) -> [T]?
func getArray(_ key: String) -> JSArray?
}
extension JSArrayContainer {
public func getArray(_ key: String, _ defaultValue: JSArray) -> JSArray {
return getArray(key) ?? defaultValue
}
public func getArray<T>(_ key: String, _ ofType: T.Type) -> [T]? {
return getArray(key) as? [T]
}
}
// dictionary types
public protocol JSObjectContainer {
func getObject(_ key: String, _ defaultValue: JSObject) -> JSObject
func getObject(_ key: String) -> JSObject?
}
extension JSObjectContainer {
public func getObject(_ key: String, _ defaultValue: JSObject) -> JSObject {
return getObject(key) ?? defaultValue
}
}
public protocol JSValueContainer: JSStringContainer, JSBoolContainer, JSIntContainer, JSFloatContainer,
JSDoubleContainer, JSDateContainer, JSArrayContainer, JSObjectContainer {
static var jsDateFormatter: ISO8601DateFormatter { get }
var data: JSObject { get }
}
extension JSValueContainer {
public func getValue(_ key: String) -> JSValue? {
return data[key]
}
public func getString(_ key: String) -> String? {
return data[key] as? String
}
public func getBool(_ key: String) -> Bool? {
return data[key] as? Bool
}
public func getInt(_ key: String) -> Int? {
return data[key] as? Int
}
public func getFloat(_ key: String) -> Float? {
if let floatValue = data[key] as? Float {
return floatValue
} else if let doubleValue = data[key] as? Double {
return Float(doubleValue)
}
return nil
}
public func getDouble(_ key: String) -> Double? {
return data[key] as? Double
}
public func getDate(_ key: String) -> Date? {
if let isoString = data[key] as? String {
return Self.jsDateFormatter.date(from: isoString)
}
return data[key] as? Date
}
public func getArray(_ key: String) -> JSArray? {
return data[key] as? JSArray
}
public func getObject(_ key: String) -> JSObject? {
return data[key] as? JSObject
}
}
@objc protocol BridgedJSValueContainer: NSObjectProtocol {
static var jsDateFormatter: ISO8601DateFormatter { get }
var dictionaryRepresentation: NSDictionary { get }
}
/*
Simply casting objects from foundation class clusters (such as __NSArrayM)
doesn't work with the JSValue protocol and will always fail. So we need to
recursively and explicitly convert each value in the dictionary.
*/
public enum JSTypes { }
extension JSTypes {
public static func coerceDictionaryToJSObject(_ dictionary: NSDictionary?, formattingDatesAsStrings: Bool = false) -> JSObject? {
return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject
}
public static func coerceDictionaryToJSObject(_ dictionary: [AnyHashable: Any]?, formattingDatesAsStrings: Bool = false) -> JSObject? {
return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject
}
public static func coerceArrayToJSArray(_ array: [Any]?, formattingDatesAsStrings: Bool = false) -> JSArray? {
return array?.compactMap { coerceToJSValue($0, formattingDates: formattingDatesAsStrings) }
}
}
private let dateStringFormatter = ISO8601DateFormatter()
// We need a large switch statement because we have a lot of types.
// swiftlint:disable:next cyclomatic_complexity
private func coerceToJSValue(_ value: Any?, formattingDates: Bool) -> JSValue? {
guard let value = value else {
return nil
}
switch value {
case let stringValue as String:
return stringValue
case let numberValue as NSNumber:
return numberValue
case let boolValue as Bool:
return boolValue
case let intValue as Int:
return intValue
case let floatValue as Float:
return floatValue
case let doubleValue as Double:
return doubleValue
case let dateValue as Date:
if formattingDates {
return dateStringFormatter.string(from: dateValue)
}
return dateValue
case let nullValue as NSNull:
return nullValue
case let arrayValue as NSArray:
return arrayValue.compactMap { coerceToJSValue($0, formattingDates: formattingDates) }
case let dictionaryValue as NSDictionary:
let keys = dictionaryValue.allKeys.compactMap { $0 as? String }
var result: JSObject = [:]
for key in keys {
result[key] = coerceToJSValue(dictionaryValue[key], formattingDates: formattingDates)
}
return result
default:
return nil
}
}

View File

@@ -5,15 +5,31 @@
import WebKit
import os.log
struct RegisterListenerArgs: Decodable {
let event: String
let handler: Channel
}
struct RemoveListenerArgs: Decodable {
let event: String
let channelId: UInt64
}
open class Plugin: NSObject {
public let manager: PluginManager = PluginManager.shared
public var config: JSObject = [:]
var config: String = "{}"
private var listeners = [String: [Channel]]()
internal func setConfig(_ config: JSObject) {
internal func setConfig(_ config: String) {
self.config = config
}
public func parseConfig<T: Decodable>(_ type: T.Type) throws -> T {
let jsonData = self.config.data(using: .utf8)!
let decoder = JSONDecoder()
return try decoder.decode(type, from: jsonData)
}
@objc open func load(webview: WKWebView) {}
@objc open func checkPermissions(_ invoke: Invoke) {
@@ -32,38 +48,32 @@ open class Plugin: NSObject {
}
}
@objc func registerListener(_ invoke: Invoke) {
guard let event = invoke.getString("event") else {
invoke.reject("`event` not provided")
return
}
guard let channel = invoke.getChannel("handler") else {
invoke.reject("`handler` not provided")
return
public func trigger<T: Encodable>(_ event: String, data: T) throws {
if let eventListeners = listeners[event] {
for channel in eventListeners {
try channel.send(data)
}
}
}
if var eventListeners = listeners[event] {
eventListeners.append(channel)
@objc func registerListener(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(RegisterListenerArgs.self)
if var eventListeners = listeners[args.event] {
eventListeners.append(args.handler)
} else {
listeners[event] = [channel]
listeners[args.event] = [args.handler]
}
invoke.resolve()
}
@objc func removeListener(_ invoke: Invoke) {
guard let event = invoke.getString("event") else {
invoke.reject("`event` not provided")
return
}
@objc func removeListener(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(RemoveListenerArgs.self)
if let eventListeners = listeners[event] {
guard let channelId = invoke.getInt("channelId") else {
invoke.reject("`channelId` not provided")
return
}
if let eventListeners = listeners[args.event] {
listeners[event] = eventListeners.filter { $0.id != channelId }
listeners[args.event] = eventListeners.filter { $0.id != args.channelId }
}
invoke.resolve()

View File

@@ -46,7 +46,7 @@ public class PluginManager {
}
}
func load<P: Plugin>(name: String, plugin: P, config: JSObject, webview: WKWebView?) {
func load<P: Plugin>(name: String, plugin: P, config: String, webview: WKWebView?) {
plugin.setConfig(config)
let handle = PluginHandle(plugin: plugin)
if let webview = webview {
@@ -95,11 +95,11 @@ extension PluginManager: NSCopying {
}
@_cdecl("register_plugin")
func registerPlugin(name: SRString, plugin: NSObject, config: NSDictionary?, webview: WKWebView?) {
func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) {
PluginManager.shared.load(
name: name.toString(),
plugin: plugin as! Plugin,
config: JSTypes.coerceDictionaryToJSObject(config ?? [:], formattingDatesAsStrings: true)!,
config: config.toString(),
webview: webview
)
}
@@ -115,34 +115,20 @@ func runCommand(
id: Int,
name: SRString,
command: SRString,
data: NSDictionary,
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>?) -> Void,
data: SRString,
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>) -> Void,
sendChannelData: @escaping @convention(c) (UInt64, UnsafePointer<CChar>) -> Void
) {
let callbackId: UInt64 = 0
let errorId: UInt64 = 1
let invoke = Invoke(
command: command.toString(), callback: callbackId, error: errorId,
sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in
sendResponse: { (fn: UInt64, payload: String?) -> Void in
let success = fn == callbackId
var payloadJson: String = ""
do {
try payloadJson =
payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
} catch {
payloadJson = "`\(error)`"
}
callback(id, success, payloadJson.cString(using: String.Encoding.utf8))
callback(id, success, payload ?? "null")
},
sendChannelData: { (id: UInt64, payload: JsonValue) -> Void in
var payloadJson: String = ""
do {
try payloadJson =
payload.jsonRepresentation() ?? "`Failed to serialize payload`"
} catch {
payloadJson = "`\(error)`"
}
sendChannelData(id, payloadJson)
}, data: JSTypes.coerceDictionaryToJSObject(data, formattingDatesAsStrings: true))
sendChannelData: { (id: UInt64, payload: String) -> Void in
sendChannelData(id, payload)
}, data: data.toString())
PluginManager.shared.invoke(name: name.toString(), invoke: invoke)
}

View File

@@ -2,9 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use cocoa::base::{id, nil, NO, YES};
use objc::*;
use serde_json::Value as JsonValue;
use swift_rs::{swift, SRString, SwiftArg};
use std::{
@@ -38,143 +35,14 @@ swift!(pub fn run_plugin_command(
id: i32,
name: &SRString,
method: &SRString,
data: *const c_void,
data: &SRString,
callback: PluginMessageCallback,
send_channel_data_callback: ChannelSendDataCallback
));
swift!(pub fn register_plugin(
name: &SRString,
plugin: *const c_void,
config: *const c_void,
config: &SRString,
webview: *const c_void
));
swift!(pub fn on_webview_created(webview: *const c_void, controller: *const c_void));
pub fn json_to_dictionary(json: &JsonValue) -> id {
if let serde_json::Value::Object(map) = json {
unsafe {
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
let data: id = msg_send![dictionary, init];
for (key, value) in map {
add_json_entry_to_dictionary(data, key, value);
}
data
}
} else {
nil
}
}
const UTF8_ENCODING: usize = 4;
struct NSString(id);
impl NSString {
fn new(s: &str) -> Self {
// Safety: objc runtime calls are unsafe
NSString(unsafe {
let ns_string: id = msg_send![class!(NSString), alloc];
let ns_string: id = msg_send![ns_string,
initWithBytes:s.as_ptr()
length:s.len()
encoding:UTF8_ENCODING];
// The thing is allocated in rust, the thing must be set to autorelease in rust to relinquish control
// or it can not be released correctly in OC runtime
let _: () = msg_send![ns_string, autorelease];
ns_string
})
}
}
unsafe fn add_json_value_to_array(array: id, value: &JsonValue) {
match value {
JsonValue::Null => {
let null: id = msg_send![class!(NSNull), null];
let () = msg_send![array, addObject: null];
}
JsonValue::Bool(val) => {
let value = if *val { YES } else { NO };
let v: id = msg_send![class!(NSNumber), numberWithBool: value];
let () = msg_send![array, addObject: v];
}
JsonValue::Number(val) => {
let number: id = if let Some(v) = val.as_i64() {
msg_send![class!(NSNumber), numberWithInteger: v]
} else if let Some(v) = val.as_u64() {
msg_send![class!(NSNumber), numberWithUnsignedLongLong: v]
} else if let Some(v) = val.as_f64() {
msg_send![class!(NSNumber), numberWithDouble: v]
} else {
unreachable!()
};
let () = msg_send![array, addObject: number];
}
JsonValue::String(val) => {
let () = msg_send![array, addObject: NSString::new(val)];
}
JsonValue::Array(val) => {
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
let inner_array: id = msg_send![nsarray, init];
for value in val {
add_json_value_to_array(inner_array, value);
}
let () = msg_send![array, addObject: inner_array];
}
JsonValue::Object(val) => {
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
let data: id = msg_send![dictionary, init];
for (key, value) in val {
add_json_entry_to_dictionary(data, key, value);
}
let () = msg_send![array, addObject: data];
}
}
}
unsafe fn add_json_entry_to_dictionary(data: id, key: &str, value: &JsonValue) {
let key = NSString::new(key);
match value {
JsonValue::Null => {
let null: id = msg_send![class!(NSNull), null];
let () = msg_send![data, setObject:null forKey: key];
}
JsonValue::Bool(val) => {
let flag = if *val { YES } else { NO };
let value: id = msg_send![class!(NSNumber), numberWithBool: flag];
let () = msg_send![data, setObject:value forKey: key];
}
JsonValue::Number(val) => {
let number: id = if let Some(v) = val.as_i64() {
msg_send![class!(NSNumber), numberWithInteger: v]
} else if let Some(v) = val.as_u64() {
msg_send![class!(NSNumber), numberWithUnsignedLongLong: v]
} else if let Some(v) = val.as_f64() {
msg_send![class!(NSNumber), numberWithDouble: v]
} else {
unreachable!()
};
let () = msg_send![data, setObject:number forKey: key];
}
JsonValue::String(val) => {
let () = msg_send![data, setObject:NSString::new(val) forKey: key];
}
JsonValue::Array(val) => {
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
let array: id = msg_send![nsarray, init];
for value in val {
add_json_value_to_array(array, value);
}
let () = msg_send![data, setObject:array forKey: key];
}
JsonValue::Object(val) => {
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
let inner_data: id = msg_send![dictionary, init];
for (key, value) in val {
add_json_entry_to_dictionary(inner_data, key, value);
}
let () = msg_send![data, setObject:inner_data forKey: key];
}
}
}

View File

@@ -1,98 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::Runtime;
use jni::{
errors::Error as JniError,
objects::{JObject, JValueOwned},
JNIEnv,
};
use serde_json::Value as JsonValue;
use tauri_runtime::RuntimeHandle;
fn json_to_java<'a, R: Runtime>(
env: &mut JNIEnv<'a>,
activity: &JObject<'_>,
runtime_handle: &R::Handle,
json: &JsonValue,
) -> Result<(&'static str, JValueOwned<'a>), JniError> {
let (class, v) = match json {
JsonValue::Null => ("Ljava/lang/Object;", JObject::null().into()),
JsonValue::Bool(val) => ("Z", (*val).into()),
JsonValue::Number(val) => {
if let Some(v) = val.as_i64() {
("J", v.into())
} else if let Some(v) = val.as_f64() {
("D", v.into())
} else {
("Ljava/lang/Object;", JObject::null().into())
}
}
JsonValue::String(val) => (
"Ljava/lang/Object;",
JObject::from(env.new_string(val)?).into(),
),
JsonValue::Array(val) => {
let js_array_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSArray")?;
let data = env.new_object(js_array_class, "()V", &[])?;
for v in val {
let (signature, val) = json_to_java::<R>(env, activity, runtime_handle, v)?;
env.call_method(
&data,
"put",
format!("({signature})Lorg/json/JSONArray;"),
&[val.borrow()],
)?;
}
("Ljava/lang/Object;", data.into())
}
JsonValue::Object(val) => {
let data = {
let js_object_class =
runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?;
env.new_object(js_object_class, "()V", &[])?
};
for (key, value) in val {
let (signature, val) = json_to_java::<R>(env, activity, runtime_handle, value)?;
let key = env.new_string(key)?;
env.call_method(
&data,
"put",
format!("(Ljava/lang/String;{signature})Lapp/tauri/plugin/JSObject;"),
&[(&key).into(), val.borrow()],
)?;
}
("Ljava/lang/Object;", data.into())
}
};
Ok((class, v))
}
pub fn to_jsobject<'a, R: Runtime>(
env: &mut JNIEnv<'a>,
activity: &JObject<'_>,
runtime_handle: &R::Handle,
json: &JsonValue,
) -> Result<JValueOwned<'a>, JniError> {
if let JsonValue::Object(_) = json {
json_to_java::<R>(env, activity, runtime_handle, json).map(|(_class, data)| data)
} else {
Ok(empty_object::<R>(env, activity, runtime_handle)?.into())
}
}
fn empty_object<'a, R: Runtime>(
env: &mut JNIEnv<'a>,
activity: &JObject<'_>,
runtime_handle: &R::Handle,
) -> Result<JObject<'a>, JniError> {
// currently the Kotlin lib cannot handle nulls or raw values, it must be an object
let js_object_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?;
let data = env.new_object(js_object_class, "()V", &[])?;
Ok(data)
}

View File

@@ -88,8 +88,6 @@ pub mod window;
use tauri_runtime as runtime;
#[cfg(target_os = "ios")]
mod ios;
#[cfg(target_os = "android")]
mod jni_helpers;
#[cfg(desktop)]
pub mod menu;
/// Path APIs.

View File

@@ -168,7 +168,7 @@ impl<R: Runtime, C: DeserializeOwned> PluginApi<R, C> {
crate::ios::register_plugin(
&name.into(),
init_fn(),
crate::ios::json_to_dictionary(&config) as _,
&serde_json::to_string(&config).unwrap().as_str().into(),
w.inner() as _,
)
};
@@ -181,7 +181,10 @@ impl<R: Runtime, C: DeserializeOwned> PluginApi<R, C> {
crate::ios::register_plugin(
&self.name.into(),
init_fn(),
crate::ios::json_to_dictionary(&self.raw_config) as _,
&serde_json::to_string(&self.raw_config)
.unwrap()
.as_str()
.into(),
std::ptr::null(),
)
};
@@ -230,17 +233,16 @@ impl<R: Runtime, C: DeserializeOwned> PluginApi<R, C> {
.l()?;
let plugin_name = env.new_string(plugin_name)?;
let config =
crate::jni_helpers::to_jsobject::<R>(env, activity, &runtime_handle, plugin_config)?;
let config = env.new_string(&serde_json::to_string(plugin_config).unwrap())?;
env.call_method(
plugin_manager,
"load",
"(Landroid/webkit/WebView;Ljava/lang/String;Lapp/tauri/plugin/Plugin;Lapp/tauri/plugin/JSObject;)V",
"(Landroid/webkit/WebView;Ljava/lang/String;Lapp/tauri/plugin/Plugin;Ljava/lang/String;)V",
&[
webview.into(),
(&plugin_name).into(),
(&plugin).into(),
config.borrow()
(&config).into(),
],
)?;
@@ -381,7 +383,7 @@ pub(crate) fn run_command<R: Runtime, C: AsRef<str>, F: FnOnce(PluginResponse) +
id,
&name.into(),
&command.as_ref().into(),
crate::ios::json_to_dictionary(&payload) as _,
&serde_json::to_string(&payload).unwrap().as_str().into(),
crate::ios::PluginMessageCallback(plugin_command_response_handler),
crate::ios::ChannelSendDataCallback(send_channel_data_handler),
);
@@ -409,13 +411,12 @@ pub(crate) fn run_command<
plugin: &str,
command: String,
payload: &serde_json::Value,
runtime_handle: R::Handle,
env: &mut JNIEnv<'_>,
activity: &JObject<'_>,
) -> Result<(), JniError> {
let plugin = env.new_string(plugin)?;
let command = env.new_string(&command)?;
let data = crate::jni_helpers::to_jsobject::<R>(env, activity, &runtime_handle, payload)?;
let data = env.new_string(&serde_json::to_string(payload).unwrap())?;
let plugin_manager = env
.call_method(
activity,
@@ -428,12 +429,12 @@ pub(crate) fn run_command<
env.call_method(
plugin_manager,
"runCommand",
"(ILjava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;)V",
"(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
&[
id.into(),
(&plugin).into(),
(&command).into(),
data.borrow(),
(&data).into(),
],
)?;
@@ -449,7 +450,6 @@ pub(crate) fn run_command<
let id: i32 = PENDING_PLUGIN_CALLS_ID.fetch_add(1, Ordering::Relaxed);
let plugin_name = name.to_string();
let command = command.as_ref().to_string();
let handle_ = handle.clone();
PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
@@ -458,7 +458,7 @@ pub(crate) fn run_command<
.insert(id, Box::new(handler.clone()));
handle.run_on_android_context(move |env, activity, _webview| {
if let Err(e) = run::<R>(id, &plugin_name, command, &payload, handle_, env, activity) {
if let Err(e) = run::<R>(id, &plugin_name, command, &payload, env, activity) {
handler(Err(e.to_string().into()));
}
});

View File

@@ -27,7 +27,7 @@ struct CachedResponse {
}
pub fn get<R: Runtime>(
manager: &WindowManager<R>,
#[allow(unused_variables)] manager: &WindowManager<R>,
window_origin: &str,
web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
) -> UriSchemeProtocolHandler {

View File

@@ -82,24 +82,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_log-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e"
[[package]]
name = "android_logger"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8619b80c242aa7bd638b5c7ddd952addeecb71f69c75e33f1d47b2804f8f883a"
dependencies = [
"android_log-sys",
"env_logger",
"log",
"once_cell",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -173,7 +155,6 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-cli",
"tauri-plugin-log",
"tauri-plugin-sample",
"tiny_http",
"window-shadows",
@@ -451,16 +432,6 @@ version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "byte-unit"
version = "4.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c"
dependencies = [
"serde",
"utf8-width",
]
[[package]]
name = "bytemuck"
version = "1.14.0"
@@ -1021,16 +992,6 @@ dependencies = [
"syn 2.0.38",
]
[[package]]
name = "env_logger"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@@ -1088,15 +1049,6 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "fern"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee"
dependencies = [
"log",
]
[[package]]
name = "field-offset"
version = "0.3.6"
@@ -2018,9 +1970,6 @@ name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
dependencies = [
"value-bag",
]
[[package]]
name = "loom"
@@ -2275,15 +2224,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]]
name = "objc"
version = "0.2.7"
@@ -3579,26 +3519,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "tauri-plugin-log"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=next#4a10f218f0e1fdd66a549dc0bf16be3efb17ea49"
dependencies = [
"android_logger",
"byte-unit",
"cocoa 0.24.1",
"fern",
"log",
"objc",
"serde",
"serde_json",
"serde_repr",
"swift-rs",
"tauri",
"tauri-build",
"time",
]
[[package]]
name = "tauri-plugin-sample"
version = "0.1.0"
@@ -3753,8 +3673,6 @@ checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
dependencies = [
"deranged",
"itoa 1.0.9",
"libc",
"num_threads",
"powerfmt",
"serde",
"time-core",
@@ -4065,12 +3983,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8-width"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
[[package]]
name = "utf8parse"
version = "0.2.1"
@@ -4092,12 +4004,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe"
[[package]]
name = "version-compare"
version = "0.1.1"

View File

@@ -18,7 +18,6 @@ serde_json = "1.0"
serde = { version = "1.0", features = [ "derive" ] }
tiny_http = "0.11"
log = "0.4"
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "next" }
tauri-plugin-sample = { path = "./tauri-plugin-sample/" }
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]

View File

@@ -43,11 +43,6 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
) {
#[allow(unused_mut)]
let mut builder = builder
.plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)
.plugin(tauri_plugin_sample::init())
.setup(move |app| {
#[cfg(desktop)]

View File

@@ -6,25 +6,33 @@ package com.plugin.sample
import android.app.Activity
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Channel
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
@InvokeArg
class PingArgs {
var value: String? = null
var onEvent: Channel? = null
}
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
private val implementation = Example()
@Command
fun ping(invoke: Invoke) {
val onEvent = invoke.getChannel("onEvent")
val args = invoke.parseArgs(PingArgs::class.java)
val event = JSObject()
event.put("kind", "ping")
onEvent?.send(event)
args.onEvent?.send(event)
val value = invoke.getString("value") ?: ""
val ret = JSObject()
ret.put("value", implementation.pong(value))
ret.put("value", implementation.pong(args.value ?: "default value :("))
invoke.resolve(ret)
}
}

View File

@@ -7,13 +7,16 @@ import Tauri
import UIKit
import WebKit
class PingArgs: Decodable {
let value: String?
let onEvent: Channel?
}
class ExamplePlugin: Plugin {
@objc public func ping(_ invoke: Invoke) throws {
let onEvent = invoke.getChannel("onEvent")
onEvent?.send(["kind": "ping"])
let value = invoke.getString("value")
invoke.resolve(["value": value as Any])
let args = try invoke.parseArgs(PingArgs.self)
args.onEvent?.send(["kind": "ping"])
invoke.resolve(["value": args.value ?? ""])
}
}

View File

@@ -2,20 +2,27 @@ package {{android_package_id}}
import android.app.Activity
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
@InvokeArg
class PingArgs {
var value: String? = null
}
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
private val implementation = Example()
@Command
fun ping(invoke: Invoke) {
val value = invoke.getString("value") ?: ""
val args = invoke.parseArgs(PingArgs::class.java)
val ret = JSObject()
ret.put("value", implementation.pong(value))
ret.put("value", implementation.pong(args.value ?: "default value :("))
invoke.resolve(ret)
}
}

View File

@@ -1,16 +1,20 @@
import SwiftRs
import Tauri
import UIKit
import WebKit
import Tauri
import SwiftRs
class PingArgs: Decodable {
let value: String?
}
class ExamplePlugin: Plugin {
@objc public func ping(_ invoke: Invoke) throws {
let value = invoke.getString("value")
invoke.resolve(["value": value as Any])
}
@objc public func ping(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(PingArgs.self)
invoke.resolve(["value": args.value ?? ""])
}
}
@_cdecl("init_plugin_{{ plugin_name_snake_case }}")
func initPlugin() -> Plugin {
return ExamplePlugin()
return ExamplePlugin()
}