Compare commits

...

2 Commits

Author SHA1 Message Date
pierre 66707004e3 wip 2023-04-21 08:51:37 +02:00
pierre baa3691851 update to latest alpha 2023-04-19 16:25:06 +02:00
38 changed files with 2863 additions and 601 deletions
Generated
+10 -11
View File
@@ -3912,19 +3912,8 @@ name = "nym-socks5-client-core"
version = "0.1.0"
dependencies = [
"dirs",
"futures",
"log",
"nym-bandwidth-controller",
"nym-client-core",
"nym-config",
"nym-credential-storage",
"nym-network-defaults",
"nym-service-providers-common",
"nym-socks5-proxy-helpers",
"nym-socks5-requests",
"nym-sphinx",
"nym-task",
"nym-validator-client",
"pin-project",
"rand 0.7.3",
"serde",
@@ -4318,6 +4307,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.25.2+1.1.1t"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320708a054ad9b3bf314688b5db87cf4d6683d64cfc835e2337924ae62bf4431"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.82"
@@ -4327,6 +4325,7 @@ dependencies = [
"autocfg 1.1.0",
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
+1 -1
View File
@@ -54,7 +54,7 @@ features = ["time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
version = "0.14"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
[target."cfg(all(not(target_arch = \"wasm32\"),not(target_os = \"android\")))".dependencies.sqlx]
version = "0.6.2"
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
optional = true
@@ -50,6 +50,10 @@ itertools = { version = "0.10", optional = true }
zeroize = { version = "1.5.7", optional = true, features = ["zeroize_derive"] }
cosmwasm-std = { workspace = true, optional = true }
[target.'cfg(android)'.dependencies.reqwest]
version = "0.11"
features = ["json", "native-tls-vendored"]
[dev-dependencies]
bip39 = { workspace = true }
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support", features = ["rpc", "bip32"] }
+5
View File
@@ -16,6 +16,11 @@ tap = "1.0.1"
tokio = { version = "1.24.1", features = ["rt-multi-thread", "net", "signal"] }
nym-client-core = { path = "../client-core", features = ["fs-surb-storage"] }
[target.'cfg(target_os = "android")'.dependencies.nym-client-core]
path = "../client-core"
features = []
futures = "0.3"
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
nym-config = { path = "../config" }
+5
View File
@@ -93,6 +93,11 @@ nym-validator-client = { path = "../common/client-libs/validator-client", featur
] }
nym-bin-common = { path = "../common/bin-common" }
[target.'cfg(target_os = "android")'.dependencies.reqwest]
version = "0.11.11"
features = ["json", "native-tls-vendored"]
[features]
no-reward = []
generate-ts = ["ts-rs"]
+4 -4
View File
@@ -9,7 +9,7 @@
"webpack:dev:onlyThis": "yarn webpack serve --config webpack.dev.js",
"webpack:prod": "yarn webpack --progress --config webpack.prod.js",
"tauri": "tauri",
"tauri:dev:android": "WRY_ANDROID_PACKAGE=net.nymtech.nym_connect WRY_ANDROID_LIBRARY=nym_connect cargo tauri android dev",
"tauri:dev:android": "cargo tauri android dev",
"tauri:build:android": "WRY_ANDROID_PACKAGE=net.nymtech.nym_connect WRY_ANDROID_LIBRARY=nym_connect cargo tauri android build",
"dev:android": "run-p webpack:dev:onlyThis tauri:dev:android",
"prebuild": "yarn --cwd ../.. build",
@@ -27,7 +27,7 @@
"clean:node": "rm -rf node_modules",
"clean:rust": "cargo clean --manifest-path src-tauri/Cargo.toml",
"clean:android": "cd src-tauri/gen/android/nym_connect/ && gradlew clean",
"clean:": "run-p clean:node clean:rust clean:android"
"clean": "run-p clean:node clean:rust clean:android"
},
"dependencies": {
"@emotion/react": "^11.7.0",
@@ -39,7 +39,7 @@
"@mui/system": ">= 5",
"@mui/lab": "^5.0.0-alpha.72",
"@nymproject/react": "^1.0.0",
"@tauri-apps/api": "^2.0.0-alpha.0",
"@tauri-apps/api": "^2.0.0-alpha.3",
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
"clsx": "^1.1.1",
"luxon": "^2.3.0",
@@ -64,7 +64,7 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@storybook/react": "^6.5.15",
"@svgr/webpack": "^6.1.1",
"@tauri-apps/cli": "^2.0.0-alpha.2",
"@tauri-apps/cli": "^2.0.0-alpha.8",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/jest": "^27.0.1",
+934 -523
View File
File diff suppressed because it is too large Load Diff
+5 -9
View File
@@ -16,13 +16,9 @@ rust-version = "1.58"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0.0-alpha.1", features = [] }
tauri-build = { version = "2.0.0-alpha.4", features = [] }
# tauri-build = { git = "https://github.com/tauri-apps/tauri", branch = "next", features = [] }
# TODO untill new tauri version includes https://github.com/tauri-apps/tauri-mobile/pull/111
[patch.crates-io]
tauri-mobile = { git = "https://github.com/tauri-apps/tauri-mobile", branch = "dev" }
[dependencies]
anyhow = "1.0"
bip39 = { version = "2.0.0", features = ["zeroize"] }
@@ -35,14 +31,14 @@ itertools = "0.10.5"
log = { version = "0.4", features = ["serde"] }
pretty_env_logger = "0.4.0"
rand = "0.8"
reqwest = { version = "0.11", features = ["json", "socks"] }
reqwest = { version = "0.11", features = ["json", "socks", "native-tls-vendored"] }
rust-embed = { version = "6.4.2", features = ["include-exclude"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
tap = "1.0.1"
# tauri = { git = "https://github.com/tauri-apps/tauri", branch = "next", features = ["clipboard-write-text", "native-tls-vendored", "notification-all", "shell-open"] }
tauri = { version = "2.0.0-alpha.3", features = ["clipboard-write-text", "native-tls-vendored", "notification-all", "shell-open"] }
tauri = { version = "2.0.0-alpha.8", features = ["clipboard-write-text", "native-tls-vendored", "notification-all", "shell-open"] }
tendermint-rpc = "0.23.0"
thiserror = "1.0"
time = { version = "0.3.17", features = ["local-offset"] }
@@ -50,14 +46,14 @@ tokio = { version = "1.24.1", features = ["sync", "time"] }
url = "2.2"
yaml-rust = "0.4"
nym-client-core = { path = "../../../common/client-core", features = ["mobile"], default-features = false }
nym-client-core = { path = "../../../common/client-core", features = [] }
nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common"}
nym-config-common = { path = "../../../common/config", package = "nym-config" }
nym-credential-storage = { path = "../../../common/credential-storage" }
nym-crypto = { path = "../../../common/crypto" }
nym-bin-common = { path = "../../../common/bin-common"}
nym-socks5-client-core = { path = "../../../common/socks5-client-core", default-features = false }
nym-socks5-client-core = { path = "../../../common/socks5-client-core", default-features = true }
nym-task = { path = "../../../common/task" }
[dev-dependencies]
@@ -0,0 +1,44 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "app.tauri"
compileSdk = 33
defaultConfig {
minSdk = 21
targetSdk = 33
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("proguard-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
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")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
@@ -0,0 +1,25 @@
-keep class app.tauri.** {
@app.tauri.JniMethod public <methods>;
native <methods>;
}
-keep class app.tauri.plugin.JSArray {
public <init>(...);
}
-keepclassmembers class org.json.JSONArray {
public put(...);
}
-keep class app.tauri.plugin.JSObject {
public <init>(...);
public put(...);
}
-keep @app.tauri.annotation.TauriPlugin public class * {
@app.tauri.annotation.Command public <methods>;
@app.tauri.annotation.PermissionCallback <methods>;
@app.tauri.annotation.ActivityCallback <methods>;
@app.tauri.annotation.Permission <methods>;
public <init>(...);
}
@@ -0,0 +1,28 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("app.tauri.test", appContext.packageName)
}
}
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,201 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.OpenableColumns
import java.io.File
import java.io.FileOutputStream
import kotlin.math.min
internal class FsUtils {
companion object {
fun getFileUrlForUri(context: Context, uri: Uri): String? {
// DocumentProvider
if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
val docId: String = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
val type = split[0]
if ("primary".equals(type, ignoreCase = true)) {
return legacyPrimaryPath(split[1])
} else {
val splitIndex = docId.indexOf(':', 1)
val tag = docId.substring(0, splitIndex)
val path = docId.substring(splitIndex + 1)
val nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag)
if (nonPrimaryVolume != null) {
val result = "$nonPrimaryVolume/$path"
val file = File(result)
return if (file.exists() && file.canRead()) {
result
} else null
}
}
} else if (isDownloadsDocument(uri)) {
val id: String = DocumentsContract.getDocumentId(uri)
val contentUri: Uri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"),
java.lang.Long.valueOf(id)
)
return getDataColumn(context, contentUri, null, null)
} else if (isMediaDocument(uri)) {
val docId: String = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
if (contentUri != null) {
return getDataColumn(context, contentUri, selection, selectionArgs)
}
}
} else if ("content".equals(uri.scheme, ignoreCase = true)) {
// Return the remote address
return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(
context,
uri,
null,
null
)
} else if ("file".equals(uri.scheme, ignoreCase = true)) {
return uri.path
}
return null
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
private fun getDataColumn(
context: Context,
uri: Uri,
selection: String?,
selectionArgs: Array<String>?
): String? {
var path: String? = null
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
path = cursor.getString(index)
}
} catch (ex: IllegalArgumentException) {
return getCopyFilePath(uri, context)
} finally {
cursor?.close()
}
return path ?: getCopyFilePath(uri, context)
}
private fun getCopyFilePath(uri: Uri, context: Context): String? {
val cursor = context.contentResolver.query(uri, null, null, null, null)!!
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
val name = cursor.getString(nameIndex)
val file = File(context.filesDir, name)
try {
val inputStream = context.contentResolver.openInputStream(uri)
val outputStream = FileOutputStream(file)
var read: Int
val maxBufferSize = 1024 * 1024
val bufferSize = min(inputStream!!.available(), maxBufferSize)
val buffers = ByteArray(bufferSize)
while (inputStream.read(buffers).also { read = it } != -1) {
outputStream.write(buffers, 0, read)
}
inputStream.close()
outputStream.close()
} catch (e: Exception) {
return null
} finally {
cursor.close()
}
return file.path
}
private fun legacyPrimaryPath(pathPart: String): String {
return Environment.getExternalStorageDirectory().toString() + "/" + pathPart
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
private fun getPathToNonPrimaryVolume(context: Context, tag: String): String? {
val volumes = context.externalCacheDirs
if (volumes != null) {
for (volume in volumes) {
if (volume != null) {
val path = volume.absolutePath
val index = path.indexOf(tag)
if (index != -1) {
return path.substring(0, index) + tag
}
}
}
}
return null
}
}
}
@@ -0,0 +1,8 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
@Retention(AnnotationRetention.RUNTIME)
internal annotation class JniMethod
@@ -0,0 +1,85 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java
import android.text.TextUtils;
import android.util.Log;
class Logger {
companion object {
private const val LOG_TAG_CORE = "Tauri"
fun tags(vararg subtags: String): String {
return if (subtags.isNotEmpty()) {
LOG_TAG_CORE + "/" + TextUtils.join("/", subtags)
} else LOG_TAG_CORE
}
fun verbose(message: String) {
verbose(LOG_TAG_CORE, message)
}
fun verbose(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.v(tag, message)
}
fun debug(message: String) {
debug(LOG_TAG_CORE, message)
}
fun debug(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.d(tag, message)
}
fun info(message: String) {
info(LOG_TAG_CORE, message)
}
fun info(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.i(tag, message)
}
fun warn(message: String) {
warn(LOG_TAG_CORE, message)
}
fun warn(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.w(tag, message)
}
fun error(message: String) {
error(LOG_TAG_CORE, message, null)
}
fun error(message: String, e: Throwable?) {
error(LOG_TAG_CORE, message, e)
}
fun error(tag: String, message: String, e: Throwable?) {
if (!shouldLog()) {
return
}
Log.e(tag, message, e)
}
private fun shouldLog(): Boolean {
return BuildConfig.DEBUG
}
}
}
@@ -0,0 +1,78 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import android.app.Activity
import android.os.Environment
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSObject
@TauriPlugin
class PathPlugin(private val activity: Activity): Plugin(activity) {
private fun resolvePath(invoke: Invoke, path: String?) {
val obj = JSObject()
obj.put("path", path)
invoke.resolve(obj)
}
@Command
fun getAudioDir(invoke: Invoke) {
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath)
}
@Command
fun getExternalCacheDir(invoke: Invoke) {
resolvePath(invoke, activity.externalCacheDir?.absolutePath)
}
@Command
fun getConfigDir(invoke: Invoke) {
resolvePath(invoke, activity.dataDir.absolutePath)
}
@Command
fun getDataDir(invoke: Invoke) {
resolvePath(invoke, activity.dataDir.absolutePath)
}
@Command
fun getDocumentDir(invoke: Invoke) {
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)?.absolutePath)
}
@Command
fun getDownloadDir(invoke: Invoke) {
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath)
}
@Command
fun getPictureDir(invoke: Invoke) {
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.absolutePath)
}
@Command
fun getPublicDir(invoke: Invoke) {
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DCIM)?.absolutePath)
}
@Command
fun getVideoDir(invoke: Invoke) {
resolvePath(invoke, activity.externalCacheDir?.absolutePath)
}
@Command
fun getResourcesDir(invoke: Invoke) {
// TODO
resolvePath(invoke, activity.cacheDir.absolutePath)
}
@Command
fun getCacheDir(invoke: Invoke) {
resolvePath(invoke, activity.cacheDir.absolutePath)
}
}
@@ -0,0 +1,115 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.app.ActivityCompat;
import java.util.ArrayList;
object PermissionHelper {
/**
* Checks if a list of given permissions are all granted by the user
*
* @param permissions Permissions to check.
* @return True if all permissions are granted, false if at least one is not.
*/
fun hasPermissions(context: Context?, permissions: Array<String>): Boolean {
for (perm in permissions) {
if (ActivityCompat.checkSelfPermission(
context!!,
perm
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
return true
}
/**
* Check whether the given permission has been defined in the AndroidManifest.xml
*
* @param permission A permission to check.
* @return True if the permission has been defined in the Manifest, false if not.
*/
fun hasDefinedPermission(context: Context, permission: String): Boolean {
var hasPermission = false
val requestedPermissions = getManifestPermissions(context)
if (requestedPermissions != null && requestedPermissions.isNotEmpty()) {
val requestedPermissionsList = listOf(*requestedPermissions)
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
if (requestedPermissionsArrayList.contains(permission)) {
hasPermission = true
}
}
return hasPermission
}
/**
* Check whether all of the given permissions have been defined in the AndroidManifest.xml
* @param context the app context
* @param permissions a list of permissions
* @return true only if all permissions are defined in the AndroidManifest.xml
*/
fun hasDefinedPermissions(context: Context, permissions: Array<String>): Boolean {
for (permission in permissions) {
if (!hasDefinedPermission(context, permission)) {
return false
}
}
return true
}
/**
* Get the permissions defined in AndroidManifest.xml
*
* @return The permissions defined in AndroidManifest.xml
*/
private fun getManifestPermissions(context: Context): Array<String>? {
var requestedPermissions: Array<String>? = null
try {
val pm = context.packageManager
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
} else {
@Suppress("DEPRECATION")
pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
}
if (packageInfo != null) {
requestedPermissions = packageInfo.requestedPermissions
}
} catch (_: Exception) {
}
return requestedPermissions
}
/**
* Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml
*
* @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?>()
val requestedPermissions = getManifestPermissions(context)
if (requestedPermissions != null && requestedPermissions.isNotEmpty()) {
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 neededPermissions as Array<String?>
}
}
@@ -0,0 +1,21 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import java.util.*
enum class PermissionState(private val state: String) {
GRANTED("granted"), DENIED("denied"), PROMPT("prompt"), PROMPT_WITH_RATIONALE("prompt-with-rationale");
override fun toString(): String {
return state
}
companion object {
fun byState(state: String): PermissionState {
return valueOf(state.uppercase(Locale.ROOT).replace('-', '_'))
}
}
}
@@ -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.FUNCTION)
annotation class ActivityCallback
@@ -0,0 +1,19 @@
// 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)
annotation class Permission(
/**
* An array of Android permission strings.
* Eg: {Manifest.permission.ACCESS_COARSE_LOCATION}
* or {"android.permission.ACCESS_COARSE_LOCATION"}
*/
val strings: Array<String> = [],
/**
* An optional name to use instead of the Android permission string.
*/
val alias: String = ""
)
@@ -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.FUNCTION)
annotation class PermissionCallback
@@ -0,0 +1,8 @@
// 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)
annotation class Command
@@ -0,0 +1,19 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.annotation
import app.tauri.annotation.Permission
/**
* Base annotation for all Plugins
*/
@Retention(AnnotationRetention.RUNTIME)
annotation class TauriPlugin(
/**
* Permissions this plugin needs, in order to make permission requests
* easy if the plugin only needs basic permission prompting
*/
val permissions: Array<Permission> = []
)
@@ -0,0 +1,11 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
internal class InvalidCommandException : Exception {
constructor(s: String?) : super(s) {}
constructor(t: Throwable?) : super(t) {}
constructor(s: String?, t: Throwable?) : super(s, t) {}
}
@@ -0,0 +1,200 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import app.tauri.Logger
class Invoke(
val id: Long,
val command: String,
private val sendResponse: (success: PluginResult?, error: PluginResult?) -> Unit,
val data: JSObject) {
fun resolve(data: JSObject?) {
val result = PluginResult(data)
sendResponse(result, null)
}
fun resolve() {
sendResponse(null, 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("code", code)
if (null != data) {
errorResult.put("data", data)
}
} catch (jsonEx: Exception) {
Logger.error(Logger.tags("Plugin"), jsonEx.message!!, jsonEx)
}
sendResponse(null, errorResult)
}
fun reject(msg: String?, ex: Exception?, data: JSObject?) {
reject(msg, null, ex, data)
}
fun reject(msg: String?, code: String?, data: JSObject?) {
reject(msg, code, null, data)
}
fun reject(msg: String?, code: String?, ex: Exception?) {
reject(msg, code, ex, null)
}
fun reject(msg: String?, data: JSObject?) {
reject(msg, null, null, data)
}
fun reject(msg: String?, ex: Exception?) {
reject(msg, null, ex, null)
}
fun reject(msg: String?, code: String?) {
reject(msg, code, null, null)
}
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)
}
}
@@ -0,0 +1,45 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import org.json.JSONArray
import org.json.JSONException
class JSArray : JSONArray {
constructor() : super() {}
constructor(json: String?) : super(json) {}
constructor(copyFrom: Collection<*>?) : super(copyFrom) {}
constructor(array: Any?) : super(array) {}
@Suppress("UNCHECKED_CAST", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
@Throws(JSONException::class)
fun <E> toList(): List<E> {
val items: MutableList<E> = ArrayList()
var o: Any? = null
for (i in 0 until this.length()) {
this.get(i).also { o = it }
try {
items.add(this.get(i) as E)
} catch (ex: Exception) {
throw JSONException("Not all items are instances of the given type")
}
}
return items
}
companion object {
/**
* Create a new JSArray without throwing a error
*/
fun from(array: Any?): JSArray? {
try {
return JSArray(array)
} catch (ex: JSONException) {
//
}
return null
}
}
}
@@ -0,0 +1,150 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import org.json.JSONException
import org.json.JSONObject
class JSObject : JSONObject {
constructor() : super()
constructor(json: String) : super(json)
constructor(obj: JSONObject, names: Array<String>) : super(obj, names)
override fun getString(key: String): String {
return getString(key, "")
}
fun getString(key: String, defaultValue: String): String {
try {
val value = super.getString(key)
if (!super.isNull(key)) {
return value
}
} catch (_: JSONException) {
}
return defaultValue
}
fun getInteger(key: String): Int? {
return getInteger(key, null)
}
fun getInteger(key: String, defaultValue: Int?): Int? {
try {
return super.getInt(key)
} catch (_: JSONException) {
}
return defaultValue
}
fun getBoolean(key: String, defaultValue: Boolean?): Boolean? {
try {
return super.getBoolean(key)
} catch (_: JSONException) {
}
return defaultValue
}
/**
* Fetch boolean from jsonObject
*/
fun getBool(key: String): Boolean? {
return getBoolean(key, null)
}
fun getJSObject(name: String): JSObject? {
try {
return getJSObject(name, null)
} catch (e: JSONException) {
}
return null
}
@Throws(JSONException::class)
fun getJSObject(name: String, defaultValue: JSObject?): JSObject? {
try {
val obj = get(name)
if (obj is JSONObject) {
val keysIter = obj.keys()
val keys: MutableList<String> = ArrayList()
while (keysIter.hasNext()) {
keys.add(keysIter.next())
}
return JSObject(obj, keys.toTypedArray())
}
} catch (_: JSONException) {
}
return defaultValue
}
override fun put(key: String, value: Boolean): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
override fun put(key: String, value: Int): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
override fun put(key: String, value: Long): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
override fun put(key: String, value: Double): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
override fun put(key: String, value: Any?): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
fun put(key: String, value: String?): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
@Throws(JSONException::class)
fun putSafe(key: String, value: Any?): JSObject {
return super.put(key, value) as JSObject
}
companion object {
/**
* Convert a pathetic JSONObject into a JSObject
* @param obj
*/
@Throws(JSONException::class)
fun fromJSONObject(obj: JSONObject): JSObject {
val keysIter = obj.keys()
val keys: MutableList<String> = ArrayList()
while (keysIter.hasNext()) {
keys.add(keysIter.next())
}
return JSObject(obj, keys.toTypedArray())
}
}
}
@@ -0,0 +1,357 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.webkit.WebView
import androidx.core.app.ActivityCompat
import app.tauri.FsUtils
import app.tauri.Logger
import app.tauri.PermissionHelper
import app.tauri.PermissionState
import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
import org.json.JSONException
import java.util.*
abstract class Plugin(private val activity: Activity) {
var handle: PluginHandle? = null
open fun load(webView: WebView) {}
/**
* Start activity for result with the provided Intent and resolve calling the provided callback method name.
*
* If there is no registered activity callback for the method name passed in, the call will
* be rejected. Make sure a valid activity result callback method is registered using the
* [ActivityCallback] annotation.
*
* @param invoke the invoke object
* @param intent the intent used to start an activity
* @param callbackName the name of the callback to run when the launched activity is finished
*/
fun startActivityForResult(invoke: Invoke, intent: Intent, callbackName: String) {
handle!!.startActivityForResult(invoke, intent, callbackName)
}
/**
* Get the plugin log tags.
* @param subTags
*/
protected fun getLogTag(vararg subTags: String): String {
return Logger.tags(*subTags)
}
/**
* Gets a log tag with the plugin's class name as subTag.
*/
protected fun getLogTag(): String {
return Logger.tags(this.javaClass.simpleName)
}
/**
* Convert an URI to an URL that can be loaded by the webview.
*/
fun assetUrl(u: Uri): String {
var path = FsUtils.getFileUrlForUri(activity, u)
if (path?.startsWith("file://") == true) {
path = path.replace("file://", "")
}
return "asset://localhost$path"
}
/**
* Exported plugin method for checking the granted status for each permission
* declared on the plugin. This plugin call responds with a mapping of permissions to
* the associated granted status.
*/
@Command
@PermissionCallback
fun checkPermissions(invoke: Invoke) {
val permissionsResult: Map<String, PermissionState?> = getPermissionStates()
if (permissionsResult.isEmpty()) {
// if no permissions are defined on the plugin, resolve undefined
invoke.resolve()
} else {
val permissionsResultJSON = JSObject()
for ((key, value) in permissionsResult) {
permissionsResultJSON.put(key, value)
}
invoke.resolve(permissionsResultJSON)
}
}
/**
* Exported plugin method to request all permissions for this plugin.
* To manually request permissions within a plugin use:
* [.requestAllPermissions], or
* [.requestPermissionForAlias], or
* [.requestPermissionForAliases]
*
* @param invoke
*/
@Command
open fun requestPermissions(invoke: Invoke) {
val annotation = handle?.annotation
if (annotation != null) {
// handle permission requests for plugins defined with @TauriPlugin
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
}
// If call was made without any custom permissions, request all from plugin annotation
val aliasSet: MutableSet<String> = HashSet()
if (providedPermsList == null || providedPermsList.isEmpty()) {
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()) {
autoGrantPerms.add(perm.alias)
}
} else {
aliasSet.add(perm.alias)
}
}
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 != null && permAliases.isNotEmpty()) {
// 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())
}
invoke.resolve(permissionsResults)
} else {
// no permissions are defined on the plugin, resolve undefined
invoke.resolve()
}
}
}
/**
* Checks if the given permission alias is correctly declared in AndroidManifest.xml
* @param alias a permission alias defined on the plugin
* @return true only if all permissions associated with the given alias are declared in the manifest
*/
fun isPermissionDeclared(alias: String): Boolean {
val annotation = handle?.annotation
if (annotation != null) {
for (perm in annotation.permissions) {
if (alias.equals(perm.alias, ignoreCase = true)) {
var result = true
for (permString in perm.strings) {
result = result && PermissionHelper.hasDefinedPermission(activity, permString)
}
return result
}
}
}
Logger.error(
String.format(
"isPermissionDeclared: No alias defined for %s " + "or missing @TauriPlugin annotation.",
alias
)
)
return false
}
private fun permissionActivityResult(
invoke: Invoke,
permissionStrings: Array<String>,
callbackName: String
) {
handle!!.requestPermissions(invoke, permissionStrings, callbackName)
}
/**
* Request all of the specified permissions in the TauriPlugin annotation (if any)
*
* If there is no registered permission callback for the Invoke passed in, the call will
* be rejected. Make sure a valid permission callback method is registered using the
* [PermissionCallback] annotation.
*
* @param invoke
* @param callbackName the name of the callback to run when the permission request is complete
*/
protected fun requestAllPermissions(
invoke: Invoke,
callbackName: String
) {
val annotation = handle!!.annotation
if (annotation != null) {
val perms: HashSet<String> = HashSet()
for (perm in annotation.permissions) {
perms.addAll(perm.strings)
}
permissionActivityResult(invoke, perms.toArray(arrayOfNulls<String>(0)), callbackName)
}
}
/**
* Request permissions using an alias defined on the plugin.
*
* If there is no registered permission callback for the Invoke passed in, the call will
* be rejected. Make sure a valid permission callback method is registered using the
* [PermissionCallback] annotation.
*
* @param alias an alias defined on the plugin
* @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,
callbackName: String
) {
requestPermissionForAliases(arrayOf(alias), call, callbackName)
}
/**
* Request permissions using aliases defined on the plugin.
*
* If there is no registered permission callback for the Invoke passed in, the call will
* be rejected. Make sure a valid permission callback method is registered using the
* [PermissionCallback] annotation.
*
* @param aliases a set of aliases defined on the plugin
* @param invoke the invoke involved in originating the request
* @param callbackName the name of the callback to run when the permission request is complete
*/
fun requestPermissionForAliases(
aliases: Array<String>,
invoke: Invoke,
callbackName: String
) {
if (aliases.isEmpty()) {
Logger.error("No permission alias was provided")
return
}
val permissions = getPermissionStringsForAliases(aliases)
if (permissions.isNotEmpty()) {
permissionActivityResult(invoke, permissions, callbackName)
}
}
/**
* Gets the Android permission strings defined on the [TauriPlugin] annotation with
* the provided aliases.
*
* @param aliases aliases for permissions defined on the plugin
* @return Android permission strings associated with the provided aliases, if exists
*/
private fun getPermissionStringsForAliases(aliases: Array<String>): Array<String> {
val annotation = handle?.annotation
val perms: HashSet<String> = HashSet()
if (annotation != null) {
for (perm in annotation.permissions) {
if (aliases.contains(perm.alias)) {
perms.addAll(perm.strings)
}
}
}
return perms.toArray(arrayOfNulls(0))
}
/**
* Get the permission state for the provided permission alias.
*
* @param alias the permission alias to get
* @return the state of the provided permission alias or null
*/
fun getPermissionState(alias: String): PermissionState? {
return getPermissionStates()[alias]
}
/**
* Helper to check all permissions defined on a plugin and see the state of each.
*
* @return A mapping of permission aliases to the associated granted status.
*/
open fun getPermissionStates(): Map<String, PermissionState> {
val permissionsResults: MutableMap<String, PermissionState> = HashMap()
val annotation = handle?.annotation
if (annotation != null) {
for (perm in annotation.permissions) {
// If a permission is defined with no permission constants, return GRANTED for it.
// Otherwise, get its true state.
if (perm.strings.isEmpty() || perm.strings.size == 1 && perm.strings[0]
.isEmpty()
) {
val key = perm.alias
if (key.isNotEmpty()) {
val existingResult = permissionsResults[key]
// auto set permission state to GRANTED if the alias is empty.
if (existingResult == null) {
permissionsResults[key] = PermissionState.GRANTED
}
}
} else {
for (permString in perm.strings) {
val key = perm.alias.ifEmpty { permString }
var permissionStatus: PermissionState
if (ActivityCompat.checkSelfPermission(
activity,
permString
) == PackageManager.PERMISSION_GRANTED
) {
permissionStatus = PermissionState.GRANTED
} else {
permissionStatus = PermissionState.PROMPT
// Check if there is a cached permission state for the "Never ask again" state
val prefs =
activity.getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE)
val state = prefs.getString(permString, null)
if (state != null) {
permissionStatus = PermissionState.byState(state)
}
}
val existingResult = permissionsResults[key]
// multiple permissions with the same alias must all be true, otherwise all false.
if (existingResult == null || existingResult === PermissionState.GRANTED) {
permissionsResults[key] = permissionStatus
}
}
}
}
}
return permissionsResults
}
}
@@ -0,0 +1,146 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.webkit.WebView
import androidx.core.app.ActivityCompat
import app.tauri.PermissionHelper
import app.tauri.PermissionState
import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
import java.lang.reflect.Method
class PluginHandle(private val manager: PluginManager, val name: String, private val instance: Plugin) {
private val commands: HashMap<String, CommandData> = HashMap()
private val permissionCallbackMethods: HashMap<String, Method> = HashMap()
private val startActivityCallbackMethods: HashMap<String, Method> = HashMap()
var annotation: TauriPlugin?
var loaded = false
init {
indexMethods()
instance.handle = this
annotation = instance.javaClass.getAnnotation(TauriPlugin::class.java)
}
fun load(webView: WebView) {
instance.load(webView)
loaded = true
}
fun startActivityForResult(invoke: Invoke, intent: Intent, callbackName: String) {
manager.startActivityForResult(intent) { result ->
val method = startActivityCallbackMethods[callbackName]
if (method != null) {
method.isAccessible = true
method(instance, invoke, result)
}
}
}
fun requestPermissions(
invoke: Invoke,
permissions: Array<String>,
callbackName: String
) {
manager.requestPermissions(permissions) { result ->
if (validatePermissions(invoke, result)) {
val method = permissionCallbackMethods[callbackName]
if (method != null) {
method.isAccessible = true
method(instance, invoke)
}
}
}
}
/**
* Saves permission states and rejects if permissions were not correctly defined in
* the AndroidManifest.xml file.
*
* @param permissions
* @return true if permissions were saved and defined correctly, false if not
*/
private fun validatePermissions(
invoke: Invoke,
permissions: Map<String, Boolean>
): Boolean {
val activity = manager.activity
val prefs =
activity.getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE)
for ((permString, isGranted) in permissions) {
if (isGranted) {
// Permission granted. If previously denied, remove cached state
val state = prefs.getString(permString, null)
if (state != null) {
val editor: SharedPreferences.Editor = prefs.edit()
editor.remove(permString)
editor.apply()
}
} else {
val editor: SharedPreferences.Editor = prefs.edit()
if (ActivityCompat.shouldShowRequestPermissionRationale(
activity,
permString
)
) {
// Permission denied, can prompt again with rationale
editor.putString(permString, PermissionState.PROMPT_WITH_RATIONALE.toString())
} else {
// Permission denied permanently, store this state for future reference
editor.putString(permString, PermissionState.DENIED.toString())
}
editor.apply()
}
}
val permStrings = permissions.keys.toTypedArray()
if (!PermissionHelper.hasDefinedPermissions(activity, permStrings)) {
val builder = StringBuilder()
builder.append("Missing the following permissions in AndroidManifest.xml:\n")
val missing = PermissionHelper.getUndefinedPermissions(activity, permStrings)
for (perm in missing) {
builder.append(
"""
$perm
""".trimIndent()
)
}
invoke.reject(builder.toString())
return false
}
return true
}
@Throws(
InvalidCommandException::class,
IllegalAccessException::class
)
fun invoke(invoke: Invoke) {
val methodMeta = commands[invoke.command]
?: throw InvalidCommandException("No command " + invoke.command + " found for plugin " + instance.javaClass.name)
methodMeta.method.invoke(instance, invoke)
}
private fun indexMethods() {
val methods: Array<Method> = instance.javaClass.methods
for (method in methods) {
if (method.isAnnotationPresent(Command::class.java)) {
val command = method.getAnnotation(Command::class.java) ?: continue
val methodMeta = CommandData(method, command)
commands[method.name] = methodMeta
} else if (method.isAnnotationPresent(ActivityCallback::class.java)) {
startActivityCallbackMethods[method.name] = method
} else if (method.isAnnotationPresent(PermissionCallback::class.java)) {
permissionCallbackMethods[method.name] = method
}
}
}
}
@@ -0,0 +1,121 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import android.content.Intent
import android.webkit.WebView
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import app.tauri.JniMethod
import app.tauri.Logger
class PluginManager(val activity: AppCompatActivity) {
fun interface RequestPermissionsCallback {
fun onResult(permissions: Map<String, Boolean>)
}
fun interface ActivityResultCallback {
fun onResult(result: ActivityResult)
}
private val plugins: HashMap<String, PluginHandle> = HashMap()
private val startActivityForResultLauncher: ActivityResultLauncher<Intent>
private val requestPermissionsLauncher: ActivityResultLauncher<Array<String>>
private var requestPermissionsCallback: RequestPermissionsCallback? = null
private var startActivityForResultCallback: ActivityResultCallback? = null
init {
startActivityForResultLauncher =
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()
) { result ->
if (startActivityForResultCallback != null) {
startActivityForResultCallback!!.onResult(result)
}
}
requestPermissionsLauncher =
activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()
) { result ->
if (requestPermissionsCallback != null) {
requestPermissionsCallback!!.onResult(result)
}
}
}
fun startActivityForResult(intent: Intent, callback: ActivityResultCallback) {
startActivityForResultCallback = callback
startActivityForResultLauncher.launch(intent)
}
fun requestPermissions(
permissionStrings: Array<String>,
callback: RequestPermissionsCallback
) {
requestPermissionsCallback = callback
requestPermissionsLauncher.launch(permissionStrings)
}
@JniMethod
fun onWebViewCreated(webView: WebView) {
for ((_, plugin) in plugins) {
if (!plugin.loaded) {
plugin.load(webView)
}
}
}
@JniMethod
fun load(webView: WebView?, name: String, plugin: Plugin) {
val handle = PluginHandle(this, name, plugin)
plugins[name] = handle
if (webView != null) {
plugin.load(webView)
}
}
@JniMethod
fun postIpcMessage(webView: WebView, pluginId: String, command: String, data: JSObject, callback: Long, error: Long) {
val invoke = Invoke(callback, command, { successResult, errorResult ->
val (fn, result) = if (errorResult == null) Pair(callback, successResult) else Pair(
error,
errorResult
)
webView.evaluateJavascript("window['_$fn']($result)", null)
}, data)
dispatchPluginMessage(invoke, pluginId)
}
@JniMethod
fun runCommand(id: Int, pluginId: String, command: String, data: JSObject) {
val invoke = Invoke(id.toLong(), command, { successResult, errorResult ->
handlePluginResponse(id, successResult?.toString(), errorResult?.toString())
}, data)
dispatchPluginMessage(invoke, pluginId)
}
private fun dispatchPluginMessage(invoke: Invoke, pluginId: String) {
Logger.verbose(
Logger.tags("Plugin"),
"Tauri plugin: pluginId: $pluginId, command: ${invoke.command}"
)
try {
val plugin = plugins[pluginId]
if (plugin == null) {
invoke.reject("Plugin $pluginId not initialized")
} else {
plugins[pluginId]?.invoke(invoke)
}
} catch (e: Exception) {
invoke.reject(e.toString())
}
}
private external fun handlePluginResponse(id: Int, success: String?, error: String?)
}
@@ -0,0 +1,16 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import app.tauri.annotation.Command
import java.lang.reflect.Method
class CommandData(
val method: Method, methodDecorator: Command
) {
// The name of the method
val name: String = method.name
}
@@ -0,0 +1,67 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import android.annotation.SuppressLint
import app.tauri.Logger
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class PluginResult @JvmOverloads constructor(json: JSObject? = JSObject()) {
private val json: JSObject
init {
this.json = json ?: JSObject()
}
fun put(name: String, value: Boolean): PluginResult {
return jsonPut(name, value)
}
fun put(name: String, value: Double): PluginResult {
return jsonPut(name, value)
}
fun put(name: String, value: Int): PluginResult {
return jsonPut(name, value)
}
fun put(name: String, value: Long): PluginResult {
return jsonPut(name, value)
}
/**
* Format a date as an ISO string
*/
@SuppressLint("SimpleDateFormat")
fun put(name: String, value: Date): PluginResult {
val tz: TimeZone = TimeZone.getTimeZone("UTC")
val df: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
df.timeZone = tz
return jsonPut(name, df.format(value))
}
fun put(name: String, value: Any?): PluginResult {
return jsonPut(name, value)
}
fun put(name: String, value: PluginResult): PluginResult {
return jsonPut(name, value.json)
}
private fun jsonPut(name: String, value: Any?): PluginResult {
try {
json.put(name, value)
} catch (ex: Exception) {
Logger.error(Logger.tags("Plugin"), "", ex)
}
return this
}
override fun toString(): String {
return json.toString()
}
}
@@ -0,0 +1,21 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
@@ -0,0 +1,30 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-keep class net.nymtech.nym_connect.* {
native <methods>;
}
-keepclassmembers class net.nymtech.nym_connect.TauriActivity {
getAppClass(...);
getVersion();
}
-keep class net.nymtech.nym_connect.RustWebView {
public <init>(...);
loadUrlMainThread(...);
}
-keep class net.nymtech.nym_connect.Ipc {
public <init>(...);
@android.webkit.JavascriptInterface public <methods>;
}
-keep class net.nymtech.nym_connect.RustWebChromeClient,net.nymtech.nym_connect.RustWebViewClient {
public <init>(...);
}
-keep class net.nymtech.nym_connect.MainActivity {
public getPluginManager();
}
-keep class androidx.appcompat.app.AppCompatActivity { }
@@ -0,0 +1,4 @@
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
val implementation by configurations
dependencies {
}
@@ -0,0 +1 @@
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+53 -53
View File
@@ -4472,100 +4472,100 @@
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-1.2.0.tgz#1f196b3e012971227f41b98214c846430a4eb477"
integrity sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==
"@tauri-apps/api@^2.0.0-alpha.0":
version "2.0.0-alpha.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-alpha.0.tgz#901abbaf3b9515ba0437716ac0383de549e4e3bc"
integrity sha512-PQdy1Ao6JwKwW2/C11nP+IqnrWHB7+UgbM71zbzA1W3+1yyd9Zg+K7rzZ7f3yhvD7kdxmXUN3KgSfGeiDFzZ2A==
"@tauri-apps/api@^2.0.0-alpha.3":
version "2.0.0-alpha.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-alpha.3.tgz#7f467c188305944d67b5b6e82be0041b8b4dbbc9"
integrity sha512-F6seMDlcaxeCPy4gS0zJdp6Tet+0rd1qJi/fbKrOrhLM6Y5UtkiG1aSDnMPi+1udThSfadjhUwrLHINvfMCjzQ==
"@tauri-apps/cli-darwin-arm64@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.2.3.tgz#dae9142e683c00199f4d7e088f22b564b08b9cac"
integrity sha512-phJN3fN8FtZZwqXg08bcxfq1+X1JSDglLvRxOxB7VWPq+O5SuB8uLyssjJsu+PIhyZZnIhTGdjhzLSFhSXfLsw==
"@tauri-apps/cli-darwin-arm64@2.0.0-alpha.2":
version "2.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-alpha.2.tgz#0af2c077b80778ac4d138a0d42a5f406fcc205ee"
integrity sha512-Wu5QdZUgh0DEE0b3EKdJRkZzFoVngezxgvncQlMdXNaiKjdT767K2fB0XvQps+ycbtVLbUlG15jAwPZbWqRYGw==
"@tauri-apps/cli-darwin-arm64@2.0.0-alpha.8":
version "2.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-alpha.8.tgz#399bec52290ea85f74470bc329376142e17e9878"
integrity sha512-ZF9nkkYCDiAEKZFwjEbuqTcFVp+DBgem3edKjsZDYPQpWg0VcZOSYr0o3/RPC81T1/FAy1lq478mkcMe0efvEw==
"@tauri-apps/cli-darwin-x64@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.2.3.tgz#c6f84a11a1a7800e3e8e22c8fa5b95d0b3d1f802"
integrity sha512-jFZ/y6z8z6v4yliIbXKBXA7BJgtZVMsITmEXSuD6s5+eCOpDhQxbRkr6CA+FFfr+/r96rWSDSgDenDQuSvPAKw==
"@tauri-apps/cli-darwin-x64@2.0.0-alpha.2":
version "2.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-alpha.2.tgz#e82f01e53dabb23c700f82e904e36ac0ee635f38"
integrity sha512-e5VLsT/exSW1swUWkhCEAQ/fM8mZaUMoGeyESYtO7VfTNVglS0j+VfQ9a8taRxtOkajDZmqMDvmii4tA5I1Bbw==
"@tauri-apps/cli-darwin-x64@2.0.0-alpha.8":
version "2.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-alpha.8.tgz#4935823acf880423f6e2c70097ab6584079321b3"
integrity sha512-N5V+tbP3qeAoXrrTZXvaLIeEWKCq11tqXoNFTkIZNGNC5yQdNpZSX0LqFqzmxVR1SHiOymebvcUlx+ADIpuXGw==
"@tauri-apps/cli-linux-arm-gnueabihf@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.2.3.tgz#ecccec4c255ab32903fb36e1c746ed7b4eff0d1d"
integrity sha512-C7h5vqAwXzY0kRGSU00Fj8PudiDWFCiQqqUNI1N+fhCILrzWZB9TPBwdx33ZfXKt/U4+emdIoo/N34v3TiAOmQ==
"@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-alpha.2":
version "2.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-alpha.2.tgz#866000663c66abd8ae88682c0f6cf03734ca7c6e"
integrity sha512-+/emaFpDPuqnTIyh+WcDqCbzc/SoREFfLDyumqdnFjRU1Uvc2Z9Eo/sMVnfuUw5vDMc2EPzYtT3uiZGez65ZTA==
"@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-alpha.8":
version "2.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-alpha.8.tgz#5b46f8f38be9b0c6fe8c343e7e589c958a225775"
integrity sha512-7LHfZA99ncMDUO/wCGMtrqmDHt1uEKZiNmuzPljWLUwVvbAn0pNWNygnvPNVLEOyav5NnZSGPqT1Zmn+L6Fpyg==
"@tauri-apps/cli-linux-arm64-gnu@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.2.3.tgz#c3915de83a8fbe6f406eaa0b524a17c091a9a2cd"
integrity sha512-buf1c8sdkuUzVDkGPQpyUdAIIdn5r0UgXU6+H5fGPq/Xzt5K69JzXaeo6fHsZEZghbV0hOK+taKV4J0m30UUMQ==
"@tauri-apps/cli-linux-arm64-gnu@2.0.0-alpha.2":
version "2.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-alpha.2.tgz#157a893fa2d36e56ec16de213ed2d0a9b209833c"
integrity sha512-UHAyqt8fFbp9MEbUHSiEKjJC98w/Dta3r9auE70K+/uNpt9pnP/lGturDWWAJagRIFwYKPyqSEqL5qFcKadYqw==
"@tauri-apps/cli-linux-arm64-gnu@2.0.0-alpha.8":
version "2.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-alpha.8.tgz#56bfa1ec613bfff0aaecd8d38aaab470ae117d58"
integrity sha512-imq2MdhWdREvL2sqbU26mzH9sgSvfNWP0uvCPvPxUhK157xqdtGw+Gqm00hwnhTuT5bOFlsUNfnG2U19k1qMpA==
"@tauri-apps/cli-linux-arm64-musl@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.2.3.tgz#40f9f7cf0b4088964661fd412eff7310cb4ac605"
integrity sha512-x88wPS9W5xAyk392vc4uNHcKBBvCp0wf4H9JFMF9OBwB7vfd59LbQCFcPSu8f0BI7bPrOsyHqspWHuFL8ojQEA==
"@tauri-apps/cli-linux-arm64-musl@2.0.0-alpha.2":
version "2.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-alpha.2.tgz#452fb06b5c563500206ba4a8cd5d979b1a3c71de"
integrity sha512-3euwm11RWvmTX+ISR/Y+N0TaWTJCRIj1pDgB+r2ZptRKlVTMNTJZDTXQlyJKcWEpi1azlbdxAzRvhN8NrgDmyA==
"@tauri-apps/cli-linux-arm64-musl@2.0.0-alpha.8":
version "2.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-alpha.8.tgz#6a0e2a3f1a494481ce391d379284f688c59ba2aa"
integrity sha512-7hXEyvCosBHIN6ahkbFOI5JoyWZAulc0sYd3hWh9V/MBfU+LlPiapsJi6fdde0zew5nnzwcCtfEKkoR737tAig==
"@tauri-apps/cli-linux-x64-gnu@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.2.3.tgz#0b3e4c1fda6205dbe872f4b69506669476f60591"
integrity sha512-ZMz1jxEVe0B4/7NJnlPHmwmSIuwiD6ViXKs8F+OWWz2Y4jn5TGxWKFg7DLx5OwQTRvEIZxxT7lXHi5CuTNAxKg==
"@tauri-apps/cli-linux-x64-gnu@2.0.0-alpha.2":
version "2.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-alpha.2.tgz#155c7ab8c4c2b319bca38580e77bc6f6ffc2d664"
integrity sha512-ijJ8Wij5mVd9p6lXQ+pXoFlx3Iv1JS1KQTeySICds43xzE8esGp5+HXRXDwWqQLdVmtI77P5VRIe2ssXiaeDUg==
"@tauri-apps/cli-linux-x64-gnu@2.0.0-alpha.8":
version "2.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-alpha.8.tgz#0ae1f0c19ab8fb9878161db4d40c3cbd581e37f3"
integrity sha512-S9T/trKpXcLc7hVIv7xFrRBlaivHD/HLUz0nYAkI2izNGFmbP3QpkqQDdwpWN/fxneodNgI2/mDFC3NULClj+A==
"@tauri-apps/cli-linux-x64-musl@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.2.3.tgz#edcf8f53da50337a2e763d4fda750ef56124036c"
integrity sha512-B/az59EjJhdbZDzawEVox0LQu2ZHCZlk8rJf85AMIktIUoAZPFbwyiUv7/zjzA/sY6Nb58OSJgaPL2/IBy7E0A==
"@tauri-apps/cli-linux-x64-musl@2.0.0-alpha.2":
version "2.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-alpha.2.tgz#9965b076965bc8faf5623c9f67724ebeb9a197e7"
integrity sha512-sg8OTQfG/zJ4+6MA/+hk08hVb57iJn5VZDzBb3o6IpJ0cwtM8YDNv5C+6HWttBuxsn4oEoYxGml/FvowMfsOCg==
"@tauri-apps/cli-linux-x64-musl@2.0.0-alpha.8":
version "2.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-alpha.8.tgz#dbb668569ee71f041b38b05470697cc0ba761ff3"
integrity sha512-90mx6vSFNBbctaPaKhkH+um51gOoRJwLFOadOkklHS5Q6S6GjtSa1lmBEFyKUTAfFPtmiacuNYtoKx7nqm+z1Q==
"@tauri-apps/cli-win32-ia32-msvc@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.2.3.tgz#0592d3e4eee4685674579ba897eef1469c6f1cfe"
integrity sha512-ypdO1OdC5ugNJAKO2m3sb1nsd+0TSvMS9Tr5qN/ZSMvtSduaNwrcZ3D7G/iOIanrqu/Nl8t3LYlgPZGBKlw7Ng==
"@tauri-apps/cli-win32-ia32-msvc@2.0.0-alpha.2":
version "2.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-alpha.2.tgz#808447504cdb22a2d998ac79810cf1035c710a03"
integrity sha512-R1AmO3GEm97ptM0tjxZjZ1fLnxzN3ZeOEKc85nR7ayqVqKVhMu+dhq5lKa/Y3GdMUR6Yj9GoCnaLp2xy4bV6JQ==
"@tauri-apps/cli-win32-ia32-msvc@2.0.0-alpha.8":
version "2.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-alpha.8.tgz#f87db96860ca883efa7a7d96095583cb69a4dc5f"
integrity sha512-VAMsLJYfp6iVI7oJ+uIkfe8DKPRMtWDiSEkfFqvDyFX0WMTQl23B0AzYyapVwZc+WkTkLuoMLpIWMQCgAoQWfQ==
"@tauri-apps/cli-win32-x64-msvc@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.2.3.tgz#89f0cc36e11e56564161602cd6add155cc7b0dfb"
integrity sha512-CsbHQ+XhnV/2csOBBDVfH16cdK00gNyNYUW68isedmqcn8j+s0e9cQ1xXIqi+Hue3awp8g3ImYN5KPepf3UExw==
"@tauri-apps/cli-win32-x64-msvc@2.0.0-alpha.2":
version "2.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-alpha.2.tgz#f7890a94cd8a0cc70faa831046ca8d8576a357d9"
integrity sha512-cLJJWxCdvvQP+I0B4h6h0TMMNYISoatQu57QVxPqypbkC/lK/ljjrbD5nu7M9wTFBkLkCTGyMC7N99esCmgIBQ==
"@tauri-apps/cli-win32-x64-msvc@2.0.0-alpha.8":
version "2.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-alpha.8.tgz#d438b0231aaac35223d666d207b50c2794fc4ea6"
integrity sha512-zvWd13hRfRM0AEJJZ4t4CeB/cyru8hvbB6c+sxYDS9GPRWfHSH5dIeKoHhnMwP5fEOPZLN7VaeEP6tC88trD6g==
"@tauri-apps/cli@^1.0.5", "@tauri-apps/cli@^1.2.2":
version "1.2.3"
@@ -4582,20 +4582,20 @@
"@tauri-apps/cli-win32-ia32-msvc" "1.2.3"
"@tauri-apps/cli-win32-x64-msvc" "1.2.3"
"@tauri-apps/cli@^2.0.0-alpha.2":
version "2.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.0.0-alpha.2.tgz#bef242ce3c8b286f0ab74595c5e069b0ce7c5b4f"
integrity sha512-M5o2ESOv9jGr7oIDl3sR3Q5++DXSW4xyfxzKCyu1JVGlOc+C9Q4y0dbKhlpd0wPCAxRa0ikbfu7z8qfEhHSpVQ==
"@tauri-apps/cli@^2.0.0-alpha.8":
version "2.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.0.0-alpha.8.tgz#84772a16889b38dcd6239fe159bb5a5f2b1e1cc9"
integrity sha512-cXt6pxh7oiV8Htz7eTPor7if4aN9f9emn10+5h2Y82YzST7I7wKXsrjuk0HIyzUiqiQjUgl3iT9gh791zgtI3w==
optionalDependencies:
"@tauri-apps/cli-darwin-arm64" "2.0.0-alpha.2"
"@tauri-apps/cli-darwin-x64" "2.0.0-alpha.2"
"@tauri-apps/cli-linux-arm-gnueabihf" "2.0.0-alpha.2"
"@tauri-apps/cli-linux-arm64-gnu" "2.0.0-alpha.2"
"@tauri-apps/cli-linux-arm64-musl" "2.0.0-alpha.2"
"@tauri-apps/cli-linux-x64-gnu" "2.0.0-alpha.2"
"@tauri-apps/cli-linux-x64-musl" "2.0.0-alpha.2"
"@tauri-apps/cli-win32-ia32-msvc" "2.0.0-alpha.2"
"@tauri-apps/cli-win32-x64-msvc" "2.0.0-alpha.2"
"@tauri-apps/cli-darwin-arm64" "2.0.0-alpha.8"
"@tauri-apps/cli-darwin-x64" "2.0.0-alpha.8"
"@tauri-apps/cli-linux-arm-gnueabihf" "2.0.0-alpha.8"
"@tauri-apps/cli-linux-arm64-gnu" "2.0.0-alpha.8"
"@tauri-apps/cli-linux-arm64-musl" "2.0.0-alpha.8"
"@tauri-apps/cli-linux-x64-gnu" "2.0.0-alpha.8"
"@tauri-apps/cli-linux-x64-musl" "2.0.0-alpha.8"
"@tauri-apps/cli-win32-ia32-msvc" "2.0.0-alpha.8"
"@tauri-apps/cli-win32-x64-msvc" "2.0.0-alpha.8"
"@tauri-apps/tauri-forage@^1.0.0-beta.2":
version "1.0.0-beta.2"