mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-01-31 00:35:19 +01:00
feat(plugins): typed invoke arguments for mobile plugins (#8076)
This commit is contained in:
committed by
GitHub
parent
a74ff464bb
commit
198abe3c2c
5
.changes/android-plugin-get-config-typed.md
Normal file
5
.changes/android-plugin-get-config-typed.md
Normal 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.
|
||||
5
.changes/mobile-plugin-resolve-object.md
Normal file
5
.changes/mobile-plugin-resolve-object.md
Normal 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.
|
||||
5
.changes/mobile-plugin-typed-invoke-args.md
Normal file
5
.changes/mobile-plugin-typed-invoke-args.md
Normal 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.
|
||||
6
.changes/update-mobile-template.md
Normal file
6
.changes/update-mobile-template.md
Normal 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.
|
||||
@@ -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 }
|
||||
|
||||
@@ -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")
|
||||
|
||||
4
core/tauri/mobile/android/proguard-rules.pro
vendored
4
core/tauri/mobile/android/proguard-rules.pro
vendored
@@ -23,3 +23,7 @@
|
||||
@app.tauri.annotation.Permission <methods>;
|
||||
public <init>(...);
|
||||
}
|
||||
|
||||
-keep @app.tauri.annotation.InvokeArg public class * {
|
||||
*;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
94
examples/api/src-tauri/Cargo.lock
generated
94
examples/api/src-tauri/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?? ""])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user