Skip to content

Commit

Permalink
feat(plugins): typed invoke arguments for mobile plugins (#8076)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog authored Oct 23, 2023
1 parent a74ff46 commit 198abe3
Show file tree
Hide file tree
Showing 31 changed files with 431 additions and 934 deletions.
5 changes: 5 additions & 0 deletions .changes/android-plugin-get-config-typed.md
Original file line number Diff line number Diff line change
@@ -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: 5 additions & 0 deletions .changes/mobile-plugin-resolve-object.md
Original file line number Diff line number Diff line change
@@ -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: 5 additions & 0 deletions .changes/mobile-plugin-typed-invoke-args.md
Original file line number Diff line number Diff line change
@@ -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: 6 additions & 0 deletions .changes/update-mobile-template.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion core/tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
1 change: 1 addition & 0 deletions core/tauri/mobile/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 4 additions & 0 deletions core/tauri/mobile/android/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@
@app.tauri.annotation.Permission <methods>;
public <init>(...);
}

-keep @app.tauri.annotation.InvokeArg public class * {
*;
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,21 +95,19 @@ 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) {
if (!requestedPermissionsArrayList.contains(permission)) {
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
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,30 @@

package app.tauri.plugin

class Channel(val id: Long, private val handler: (data: JSObject) -> Unit) {
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(data)
handler(PluginResult(data).toString())
}

fun sendObject(data: Any) {
handler(objectMapper.writeValueAsString(data))
}
}
182 changes: 28 additions & 154 deletions core/tauri/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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?) {
Expand Down Expand Up @@ -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)) }
}
}
Loading

0 comments on commit 198abe3

Please sign in to comment.