diff --git a/.gitignore b/.gitignore index d0cda10..48ae1b2 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,12 @@ vite.config.ts.timestamp-* /src-tauri/target/ /src-tauri/target-codex-check*/ /src-tauri/gen/schemas/ +/src-tauri/gen/android/app/build/ +/src-tauri/gen/android/buildSrc/build/ +/src-tauri/gen/android/.gradle/ +/src-tauri/gen/android/app/.gradle/ +/src-tauri/gen/android/buildSrc/.gradle/ +/src-tauri/gen/android/build/reports/ /src-tauri/program.log* /src-tauri/recording_replay_debug_*.csv diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d46bdb9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "eskin-finger-sdk"] + path = eskin-finger-sdk + url = https://gitea.e-skin.top/yanjie/eskin-finger-sdk.git diff --git a/eskin-finger-sdk b/eskin-finger-sdk new file mode 160000 index 0000000..7053750 --- /dev/null +++ b/eskin-finger-sdk @@ -0,0 +1 @@ +Subproject commit 705375085f17c79a6fbba32c18fb7630da0b67a7 diff --git a/src-tauri/gen/android/app/proguard-tauri.pro b/src-tauri/gen/android/app/proguard-tauri.pro new file mode 100644 index 0000000..dfb876b --- /dev/null +++ b/src-tauri/gen/android/app/proguard-tauri.pro @@ -0,0 +1,5 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. + +-keep class com.lenn.tauri_serial.TauriActivity { + public app.tauri.plugin.PluginManager getPluginManager(); +} diff --git a/src-tauri/gen/android/app/src/main/assets/tauri.conf.json b/src-tauri/gen/android/app/src/main/assets/tauri.conf.json new file mode 100644 index 0000000..eb80c59 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/assets/tauri.conf.json @@ -0,0 +1 @@ +{"$schema":"https://schema.tauri.app/config/2","productName":"JE-Skin","version":"0.4.0","identifier":"com.lenn.tauri-serial","app":{"windows":[{"label":"main","create":true,"url":"index.html","dragDropEnabled":true,"center":false,"width":1366.0,"height":860.0,"resizable":true,"maximizable":true,"minimizable":true,"closable":true,"title":"JE-Skin","fullscreen":false,"focus":true,"focusable":true,"transparent":false,"maximized":false,"visible":true,"decorations":false,"alwaysOnBottom":false,"alwaysOnTop":false,"visibleOnAllWorkspaces":false,"contentProtected":false,"skipTaskbar":false,"titleBarStyle":"Visible","hiddenTitle":false,"acceptFirstMouse":false,"shadow":true,"incognito":false,"zoomHotkeysEnabled":false,"browserExtensionsEnabled":false,"useHttpsScheme":false,"javascriptDisabled":false,"allowLinkPreview":true,"disableInputAccessoryView":false,"scrollBarStyle":"default"}],"security":{"freezePrototype":false,"dangerousDisableAssetCspModification":false,"assetProtocol":{"scope":[],"enable":false},"pattern":{"use":"brownfield"},"capabilities":[]},"macOSPrivateApi":false,"withGlobalTauri":false,"enableGTKAppId":false},"build":{"devUrl":"http://localhost:1420/","frontendDist":"../build","beforeDevCommand":"npm run dev","beforeBuildCommand":"npm run build","removeUnusedCommands":false,"additionalWatchFolders":[]},"bundle":{"active":true,"targets":"all","createUpdaterArtifacts":true,"icon":["icons/32x32.png","icons/128x128.png","icons/128x128@2x.png","icons/icon.icns","icons/icon.ico"],"resources":["resources/je-skin-devkit-server.exe"],"useLocalToolsDir":false,"windows":{"digestAlgorithm":null,"certificateThumbprint":null,"timestampUrl":null,"tsp":false,"webviewInstallMode":{"type":"downloadBootstrapper","silent":true},"allowDowngrades":true,"wix":null,"nsis":{"template":"nsis/installer.nsi","headerImage":null,"sidebarImage":null,"installerIcon":"icons/icon.ico","installMode":"both","languages":null,"customLanguageFiles":null,"displayLanguageSelector":false,"compression":"lzma","startMenuFolder":null,"installerHooks":null,"minimumWebview2Version":null},"signCommand":null},"linux":{"appimage":{"bundleMediaFramework":false,"files":{}},"deb":{"files":{}},"rpm":{"release":"1","epoch":0,"files":{}}},"macOS":{"files":{},"minimumSystemVersion":"10.13","hardenedRuntime":true,"dmg":{"windowSize":{"width":660,"height":400},"appPosition":{"x":180,"y":170},"applicationFolderPosition":{"x":480,"y":170}}},"iOS":{"minimumSystemVersion":"14.0"},"android":{"minSdkVersion":24,"autoIncrementVersionCode":false}},"plugins":{}} \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/Ipc.kt b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/Ipc.kt new file mode 100644 index 0000000..996138c --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/Ipc.kt @@ -0,0 +1,33 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +@file:Suppress("unused") + +package com.lenn.tauri_serial + +import android.webkit.* + +class Ipc(val webViewClient: RustWebViewClient) { + @JavascriptInterface + fun postMessage(message: String?) { + message?.let {m -> + // we're not using WebView::getUrl() here because it needs to be executed on the main thread + // and it would slow down the Ipc + // so instead we track the current URL on the webview client + this.ipc(webViewClient.currentUrl, m) + } + } + + companion object { + init { + System.loadLibrary("tauri_demo_lib") + } + } + + private external fun ipc(url: String, message: String) + + +} diff --git a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/Logger.kt b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/Logger.kt new file mode 100644 index 0000000..90294bb --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/Logger.kt @@ -0,0 +1,89 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package com.lenn.tauri_serial + +// 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) + } + + private 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 + } + } +} diff --git a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/PermissionHelper.kt b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/PermissionHelper.kt new file mode 100644 index 0000000..c018118 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/PermissionHelper.kt @@ -0,0 +1,117 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.lenn.tauri_serial + +// 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): 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.isNullOrEmpty()) { + 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): 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? { + var requestedPermissions: Array? = 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): Array { + val undefinedPermissions = ArrayList() + val requestedPermissions = getManifestPermissions(context) + 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(undefinedPermissions.size) + undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray) + return undefinedPermissionArray + } + return neededPermissions + } +} diff --git a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/RustWebChromeClient.kt b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/RustWebChromeClient.kt new file mode 100644 index 0000000..d4b478a --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/RustWebChromeClient.kt @@ -0,0 +1,495 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +@file:Suppress("ObsoleteSdkInt", "RedundantOverride", "QueryPermissionsNeeded", "SimpleDateFormat") + +package com.lenn.tauri_serial + +// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java + +import android.Manifest +import android.app.Activity +import android.app.AlertDialog +import android.content.ActivityNotFoundException +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.view.View +import android.webkit.* +import android.widget.EditText +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.FileProvider +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* + +class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { + private interface PermissionListener { + fun onPermissionSelect(isGranted: Boolean?) + } + + private interface ActivityResultListener { + fun onActivityResult(result: ActivityResult?) + } + + private val activity: WryActivity + private var permissionLauncher: ActivityResultLauncher> + private var activityLauncher: ActivityResultLauncher + private var permissionListener: PermissionListener? = null + private var activityListener: ActivityResultListener? = null + + init { + activity = appActivity + val permissionCallback = + ActivityResultCallback { isGranted: Map -> + if (permissionListener != null) { + var granted = true + for ((_, value) in isGranted) { + if (!value) granted = false + } + permissionListener!!.onPermissionSelect(granted) + } + } + permissionLauncher = + activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback) + activityLauncher = activity.registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (activityListener != null) { + activityListener!!.onActivityResult(result) + } + } + } + + /** + * Render web content in `view`. + * + * Both this method and [.onHideCustomView] are required for + * rendering web content in full screen. + * + * @see [](https://developer.android.com/reference/android/webkit/WebChromeClient.onShowCustomView + ) */ + override fun onShowCustomView(view: View, callback: CustomViewCallback) { + callback.onCustomViewHidden() + super.onShowCustomView(view, callback) + } + + /** + * Render web content in the original Web View again. + * + * Do not remove this method--@see #onShowCustomView(View, CustomViewCallback). + */ + override fun onHideCustomView() { + super.onHideCustomView() + } + + override fun onPermissionRequest(request: PermissionRequest) { + val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + val permissionList: MutableList = ArrayList() + if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) { + permissionList.add(Manifest.permission.CAMERA) + } + if (listOf(*request.resources).contains("android.webkit.resource.AUDIO_CAPTURE")) { + permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS) + permissionList.add(Manifest.permission.RECORD_AUDIO) + } + if (permissionList.isNotEmpty() && isRequestPermissionRequired) { + val permissions = permissionList.toTypedArray() + permissionListener = object : PermissionListener { + override fun onPermissionSelect(isGranted: Boolean?) { + if (isGranted == true) { + request.grant(request.resources) + } else { + request.deny() + } + } + } + permissionLauncher.launch(permissions) + } else { + request.grant(request.resources) + } + } + + /** + * Show the browser alert modal + * @param view + * @param url + * @param message + * @param result + * @return + */ + override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean { + if (activity.isFinishing) { + return true + } + val builder = AlertDialog.Builder(view.context) + builder + .setMessage(message) + .setPositiveButton( + "OK" + ) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + result.confirm() + } + .setOnCancelListener { dialog: DialogInterface -> + dialog.dismiss() + result.cancel() + } + val dialog = builder.create() + dialog.show() + return true + } + + /** + * Show the browser confirm modal + * @param view + * @param url + * @param message + * @param result + * @return + */ + override fun onJsConfirm(view: WebView, url: String, message: String, result: JsResult): Boolean { + if (activity.isFinishing) { + return true + } + val builder = AlertDialog.Builder(view.context) + builder + .setMessage(message) + .setPositiveButton( + "OK" + ) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + result.confirm() + } + .setNegativeButton( + "Cancel" + ) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + result.cancel() + } + .setOnCancelListener { dialog: DialogInterface -> + dialog.dismiss() + result.cancel() + } + val dialog = builder.create() + dialog.show() + return true + } + + /** + * Show the browser prompt modal + * @param view + * @param url + * @param message + * @param defaultValue + * @param result + * @return + */ + override fun onJsPrompt( + view: WebView, + url: String, + message: String, + defaultValue: String, + result: JsPromptResult + ): Boolean { + if (activity.isFinishing) { + return true + } + val builder = AlertDialog.Builder(view.context) + val input = EditText(view.context) + builder + .setMessage(message) + .setView(input) + .setPositiveButton( + "OK" + ) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + val inputText1 = input.text.toString().trim { it <= ' ' } + result.confirm(inputText1) + } + .setNegativeButton( + "Cancel" + ) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + result.cancel() + } + .setOnCancelListener { dialog: DialogInterface -> + dialog.dismiss() + result.cancel() + } + val dialog = builder.create() + dialog.show() + return true + } + + /** + * Handle the browser geolocation permission prompt + * @param origin + * @param callback + */ + override fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback + ) { + super.onGeolocationPermissionsShowPrompt(origin, callback) + Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin") + val geoPermissions = + arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) + if (!PermissionHelper.hasPermissions(activity, geoPermissions)) { + permissionListener = object : PermissionListener { + override fun onPermissionSelect(isGranted: Boolean?) { + if (isGranted == true) { + callback.invoke(origin, true, false) + } else { + val coarsePermission = + arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + PermissionHelper.hasPermissions(activity, coarsePermission) + ) { + callback.invoke(origin, true, false) + } else { + callback.invoke(origin, false, false) + } + } + } + } + permissionLauncher.launch(geoPermissions) + } else { + // permission is already granted + callback.invoke(origin, true, false) + Logger.debug("onGeolocationPermissionsShowPrompt: has required permission") + } + } + + override fun onShowFileChooser( + webView: WebView, + filePathCallback: ValueCallback?>, + fileChooserParams: FileChooserParams + ): Boolean { + val acceptTypes = listOf(*fileChooserParams.acceptTypes) + val captureEnabled = fileChooserParams.isCaptureEnabled + val capturePhoto = captureEnabled && acceptTypes.contains("image/*") + val captureVideo = captureEnabled && acceptTypes.contains("video/*") + if (capturePhoto || captureVideo) { + if (isMediaCaptureSupported) { + showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo) + } else { + permissionListener = object : PermissionListener { + override fun onPermissionSelect(isGranted: Boolean?) { + if (isGranted == true) { + showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo) + } else { + Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted") + filePathCallback.onReceiveValue(null) + } + } + } + val camPermission = arrayOf(Manifest.permission.CAMERA) + permissionLauncher.launch(camPermission) + } + } else { + showFilePicker(filePathCallback, fileChooserParams) + } + return true + } + + private val isMediaCaptureSupported: Boolean + get() { + val permissions = arrayOf(Manifest.permission.CAMERA) + return PermissionHelper.hasPermissions(activity, permissions) || + !PermissionHelper.hasDefinedPermission(activity, Manifest.permission.CAMERA) + } + + private fun showMediaCaptureOrFilePicker( + filePathCallback: ValueCallback?>, + fileChooserParams: FileChooserParams, + isVideo: Boolean + ) { + val isVideoCaptureSupported = true + val shown = if (isVideo && isVideoCaptureSupported) { + showVideoCapturePicker(filePathCallback) + } else { + showImageCapturePicker(filePathCallback) + } + if (!shown) { + Logger.warn( + Logger.tags("FileChooser"), + "Media capture intent could not be launched. Falling back to default file picker." + ) + showFilePicker(filePathCallback, fileChooserParams) + } + } + + private fun showImageCapturePicker(filePathCallback: ValueCallback?>): Boolean { + val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + if (takePictureIntent.resolveActivity(activity.packageManager) == null) { + return false + } + val imageFileUri: Uri = try { + createImageFileUri() + } catch (ex: Exception) { + Logger.error("Unable to create temporary media capture file: " + ex.message) + return false + } + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri) + activityListener = object : ActivityResultListener { + override fun onActivityResult(result: ActivityResult?) { + var res: Array? = null + if (result?.resultCode == Activity.RESULT_OK) { + res = arrayOf(imageFileUri) + } + filePathCallback.onReceiveValue(res) + } + } + activityLauncher.launch(takePictureIntent) + return true + } + + private fun showVideoCapturePicker(filePathCallback: ValueCallback?>): Boolean { + val takeVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) + if (takeVideoIntent.resolveActivity(activity.packageManager) == null) { + return false + } + activityListener = object : ActivityResultListener { + override fun onActivityResult(result: ActivityResult?) { + var res: Array? = null + if (result?.resultCode == Activity.RESULT_OK) { + res = arrayOf(result.data!!.data) + } + filePathCallback.onReceiveValue(res) + } + } + activityLauncher.launch(takeVideoIntent) + return true + } + + private fun showFilePicker( + filePathCallback: ValueCallback?>, + fileChooserParams: FileChooserParams + ) { + val intent = fileChooserParams.createIntent() + if (fileChooserParams.mode == FileChooserParams.MODE_OPEN_MULTIPLE) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + if (fileChooserParams.acceptTypes.size > 1 || intent.type!!.startsWith(".")) { + val validTypes = getValidTypes(fileChooserParams.acceptTypes) + intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes) + if (intent.type!!.startsWith(".")) { + intent.type = validTypes[0] + } + } + try { + activityListener = object : ActivityResultListener { + override fun onActivityResult(result: ActivityResult?) { + val res: Array? + val resultIntent = result?.data + if (result?.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) { + val numFiles = resultIntent.clipData!!.itemCount + res = arrayOfNulls(numFiles) + for (i in 0 until numFiles) { + res[i] = resultIntent.clipData!!.getItemAt(i).uri + } + } else { + res = FileChooserParams.parseResult( + result?.resultCode ?: 0, + resultIntent + ) + } + filePathCallback.onReceiveValue(res) + } + } + activityLauncher.launch(intent) + } catch (e: ActivityNotFoundException) { + filePathCallback.onReceiveValue(null) + } + } + + private fun getValidTypes(currentTypes: Array): Array { + val validTypes: MutableList = ArrayList() + val mtm = MimeTypeMap.getSingleton() + for (mime in currentTypes) { + if (mime.startsWith(".")) { + val extension = mime.substring(1) + val extensionMime = mtm.getMimeTypeFromExtension(extension) + if (extensionMime != null && !validTypes.contains(extensionMime)) { + validTypes.add(extensionMime) + } + } else if (!validTypes.contains(mime)) { + validTypes.add(mime) + } + } + val validObj: Array = validTypes.toTypedArray() + return Arrays.copyOf( + validObj, validObj.size, + Array::class.java + ) + } + + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { + val tag: String = Logger.tags("Console") + if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) { + val msg = String.format( + "File: %s - Line %d - Msg: %s", + consoleMessage.sourceId(), + consoleMessage.lineNumber(), + consoleMessage.message() + ) + val level = consoleMessage.messageLevel().name + if ("ERROR".equals(level, ignoreCase = true)) { + Logger.error(tag, msg, null) + } else if ("WARNING".equals(level, ignoreCase = true)) { + Logger.warn(tag, msg) + } else if ("TIP".equals(level, ignoreCase = true)) { + Logger.debug(tag, msg) + } else { + Logger.info(tag, msg) + } + } + return true + } + + private fun isValidMsg(msg: String): Boolean { + return !(msg.contains("%cresult %c") || + msg.contains("%cnative %c") || + msg.equals("[object Object]", ignoreCase = true) || + msg.equals("console.groupEnd", ignoreCase = true)) + } + + @Throws(IOException::class) + private fun createImageFileUri(): Uri { + val photoFile = createImageFile(activity) + return FileProvider.getUriForFile( + activity, + activity.packageName.toString() + ".fileprovider", + photoFile + ) + } + + @Throws(IOException::class) + private fun createImageFile(activity: Activity): File { + // Create an image file name + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) + val imageFileName = "JPEG_" + timeStamp + "_" + val storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + return File.createTempFile(imageFileName, ".jpg", storageDir) + } + + override fun onReceivedTitle( + view: WebView, + title: String + ) { + handleReceivedTitle(view, title) + } + + private external fun handleReceivedTitle(webview: WebView, title: String) +} diff --git a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/RustWebView.kt b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/RustWebView.kt new file mode 100644 index 0000000..767dff0 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/RustWebView.kt @@ -0,0 +1,101 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +@file:Suppress("unused", "SetJavaScriptEnabled") + +package com.lenn.tauri_serial + +import android.annotation.SuppressLint +import android.webkit.* +import android.content.Context +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature +import kotlin.collections.Map + +@SuppressLint("RestrictedApi") +class RustWebView(context: Context, val initScripts: Array, val id: String): WebView(context) { + val isDocumentStartScriptEnabled: Boolean + + init { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.setGeolocationEnabled(true) + settings.databaseEnabled = true + settings.mediaPlaybackRequiresUserGesture = false + settings.javaScriptCanOpenWindowsAutomatically = true + + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + isDocumentStartScriptEnabled = true + for (script in initScripts) { + WebViewCompat.addDocumentStartJavaScript(this, script, setOf("*")); + } + } else { + isDocumentStartScriptEnabled = false + } + + + } + + fun loadUrlMainThread(url: String) { + post { + loadUrl(url) + } + } + + fun loadUrlMainThread(url: String, additionalHttpHeaders: Map) { + post { + loadUrl(url, additionalHttpHeaders) + } + } + + override fun loadUrl(url: String) { + if (!shouldOverride(url)) { + super.loadUrl(url); + } + } + + override fun loadUrl(url: String, additionalHttpHeaders: Map) { + if (!shouldOverride(url)) { + super.loadUrl(url, additionalHttpHeaders); + } + } + + fun loadHTMLMainThread(html: String) { + post { + super.loadData(html, "text/html", null) + } + } + + fun evalScript(id: Int, script: String) { + post { + super.evaluateJavascript(script) { result -> + onEval(id, result) + } + } + } + + fun clearAllBrowsingData() { + try { + super.getContext().deleteDatabase("webviewCache.db") + super.getContext().deleteDatabase("webview.db") + super.clearCache(true) + super.clearHistory() + super.clearFormData() + } catch (ex: Exception) { + Logger.error("Unable to create temporary media capture file: " + ex.message) + } + } + + fun getCookies(url: String): String { + val cookieManager = CookieManager.getInstance() + return cookieManager.getCookie(url) + } + + private external fun shouldOverride(url: String): Boolean + private external fun onEval(id: Int, result: String) + + +} diff --git a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/RustWebViewClient.kt b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/RustWebViewClient.kt new file mode 100644 index 0000000..ff943a4 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/RustWebViewClient.kt @@ -0,0 +1,107 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.lenn.tauri_serial + +import android.net.Uri +import android.webkit.* +import android.content.Context +import android.graphics.Bitmap +import android.os.Handler +import android.os.Looper +import androidx.webkit.WebViewAssetLoader + +class RustWebViewClient(context: Context): WebViewClient() { + private val interceptedState = mutableMapOf() + var currentUrl: String = "about:blank" + private var lastInterceptedUrl: Uri? = null + private var pendingUrlRedirect: String? = null + + private val assetLoader = WebViewAssetLoader.Builder() + .setDomain(assetLoaderDomain()) + .addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(context)) + .build() + + override fun shouldInterceptRequest( + view: WebView, + request: WebResourceRequest + ): WebResourceResponse? { + pendingUrlRedirect?.let { + Handler(Looper.getMainLooper()).post { + view.loadUrl(it) + } + pendingUrlRedirect = null + return null + } + + lastInterceptedUrl = request.url + return if (withAssetLoader()) { + assetLoader.shouldInterceptRequest(request.url) + } else { + val rustWebview = view as RustWebView; + val response = handleRequest(rustWebview.id, request, rustWebview.isDocumentStartScriptEnabled) + interceptedState[request.url.toString()] = response != null + return response + } + } + + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest + ): Boolean { + return shouldOverride(request.url.toString()) + } + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + currentUrl = url + if (interceptedState[url] == false) { + val webView = view as RustWebView + for (script in webView.initScripts) { + view.evaluateJavascript(script, null) + } + } + return onPageLoading(url) + } + + override fun onPageFinished(view: WebView, url: String) { + onPageLoaded(url) + } + + override fun onReceivedError( + view: WebView, + request: WebResourceRequest, + error: WebResourceError + ) { + // we get a net::ERR_CONNECTION_REFUSED when an external URL redirects to a custom protocol + // e.g. oauth flow, because shouldInterceptRequest is not called on redirects + // so we must force retry here with loadUrl() to get a chance of the custom protocol to kick in + if (error.errorCode == ERROR_CONNECT && request.isForMainFrame && request.url != lastInterceptedUrl) { + // prevent the default error page from showing + view.stopLoading() + // without this initial loadUrl the app is stuck + view.loadUrl(request.url.toString()) + // ensure the URL is actually loaded - for some reason there's a race condition and we need to call loadUrl() again later + pendingUrlRedirect = request.url.toString() + } else { + super.onReceivedError(view, request, error) + } + } + + companion object { + init { + System.loadLibrary("tauri_demo_lib") + } + } + + private external fun assetLoaderDomain(): String + private external fun withAssetLoader(): Boolean + private external fun handleRequest(webviewId: String, request: WebResourceRequest, isDocumentStartScriptEnabled: Boolean): WebResourceResponse? + private external fun shouldOverride(url: String): Boolean + private external fun onPageLoading(url: String) + private external fun onPageLoaded(url: String) + + +} diff --git a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/TauriActivity.kt b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/TauriActivity.kt new file mode 100644 index 0000000..3553144 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/TauriActivity.kt @@ -0,0 +1,51 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +package com.lenn.tauri_serial + +import android.content.Intent +import android.content.res.Configuration +import app.tauri.plugin.PluginManager + +abstract class TauriActivity : WryActivity() { + var pluginManager: PluginManager = PluginManager(this) + override val handleBackNavigation: Boolean = false + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + pluginManager.onNewIntent(intent) + } + + override fun onResume() { + super.onResume() + pluginManager.onResume() + } + + override fun onPause() { + super.onPause() + pluginManager.onPause() + } + + override fun onRestart() { + super.onRestart() + pluginManager.onRestart() + } + + override fun onStop() { + super.onStop() + pluginManager.onStop() + } + + override fun onDestroy() { + super.onDestroy() + pluginManager.onDestroy() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + pluginManager.onConfigurationChanged(newConfig) + } +} diff --git a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/WryActivity.kt b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/WryActivity.kt new file mode 100644 index 0000000..cf5d121 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/WryActivity.kt @@ -0,0 +1,146 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.lenn.tauri_serial + +import com.lenn.tauri_serial.RustWebView +import android.annotation.SuppressLint +import android.os.Build +import android.os.Bundle +import android.webkit.WebView +import android.view.KeyEvent +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity + +abstract class WryActivity : AppCompatActivity() { + private lateinit var mWebView: RustWebView + open val handleBackNavigation: Boolean = true + + open fun onWebViewCreate(webView: WebView) { } + + fun setWebView(webView: RustWebView) { + mWebView = webView + + if (handleBackNavigation) { + val callback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (this@WryActivity.mWebView.canGoBack()) { + this@WryActivity.mWebView.goBack() + } else { + this.isEnabled = false + this@WryActivity.onBackPressed() + this.isEnabled = true + } + } + } + onBackPressedDispatcher.addCallback(this, callback) + } + + onWebViewCreate(webView) + } + + val version: String + @SuppressLint("WebViewApiAvailability", "ObsoleteSdkInt") + get() { + // Check getCurrentWebViewPackage() directly if above Android 8 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return WebView.getCurrentWebViewPackage()?.versionName ?: "" + } + + // Otherwise manually check WebView versions + var webViewPackage = "com.google.android.webview" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + webViewPackage = "com.android.chrome" + } + try { + @Suppress("DEPRECATION") + val info = packageManager.getPackageInfo(webViewPackage, 0) + return info.versionName.toString() + } catch (ex: Exception) { + Logger.warn("Unable to get package info for '$webViewPackage'$ex") + } + + try { + @Suppress("DEPRECATION") + val info = packageManager.getPackageInfo("com.android.webview", 0) + return info.versionName.toString() + } catch (ex: Exception) { + Logger.warn("Unable to get package info for 'com.android.webview'$ex") + } + + // Could not detect any webview, return empty string + return "" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + create(this) + } + + override fun onStart() { + super.onStart() + start() + } + + override fun onResume() { + super.onResume() + resume() + } + + override fun onPause() { + super.onPause() + pause() + } + + override fun onStop() { + super.onStop() + stop() + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + focus(hasFocus) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + save() + } + + override fun onDestroy() { + super.onDestroy() + destroy() + onActivityDestroy() + } + + override fun onLowMemory() { + super.onLowMemory() + memory() + } + + fun getAppClass(name: String): Class<*> { + return Class.forName(name) + } + + companion object { + init { + System.loadLibrary("tauri_demo_lib") + } + } + + private external fun create(activity: WryActivity) + private external fun start() + private external fun resume() + private external fun pause() + private external fun stop() + private external fun save() + private external fun destroy() + private external fun onActivityDestroy() + private external fun memory() + private external fun focus(focus: Boolean) + + +} diff --git a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/proguard-wry.pro b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/proguard-wry.pro new file mode 100644 index 0000000..7d27af6 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/generated/proguard-wry.pro @@ -0,0 +1,35 @@ +# THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! + +# Copyright 2020-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +-keep class com.lenn.tauri_serial.* { + native ; +} + +-keep class com.lenn.tauri_serial.WryActivity { + public (...); + + void setWebView(com.lenn.tauri_serial.RustWebView); + java.lang.Class getAppClass(...); + java.lang.String getVersion(); +} + +-keep class com.lenn.tauri_serial.Ipc { + public (...); + + @android.webkit.JavascriptInterface public ; +} + +-keep class com.lenn.tauri_serial.RustWebView { + public (...); + + void loadUrlMainThread(...); + void loadHTMLMainThread(...); + void evalScript(...); +} + +-keep class com.lenn.tauri_serial.RustWebChromeClient,com.lenn.tauri_serial.RustWebViewClient { + public (...); +} diff --git a/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a/libtauri_demo_lib.so b/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a/libtauri_demo_lib.so new file mode 120000 index 0000000..7567d22 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a/libtauri_demo_lib.so @@ -0,0 +1 @@ +/home/lenn/Workspace/JE-Skin/src-tauri/target/aarch64-linux-android/release/libtauri_demo_lib.so \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/jniLibs/armeabi-v7a/libtauri_demo_lib.so b/src-tauri/gen/android/app/src/main/jniLibs/armeabi-v7a/libtauri_demo_lib.so new file mode 120000 index 0000000..793c5f2 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/jniLibs/armeabi-v7a/libtauri_demo_lib.so @@ -0,0 +1 @@ +/home/lenn/Workspace/JE-Skin/src-tauri/target/armv7-linux-androideabi/release/libtauri_demo_lib.so \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/jniLibs/x86/libtauri_demo_lib.so b/src-tauri/gen/android/app/src/main/jniLibs/x86/libtauri_demo_lib.so new file mode 120000 index 0000000..1c5ae81 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/jniLibs/x86/libtauri_demo_lib.so @@ -0,0 +1 @@ +/home/lenn/Workspace/JE-Skin/src-tauri/target/i686-linux-android/release/libtauri_demo_lib.so \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/jniLibs/x86_64/libtauri_demo_lib.so b/src-tauri/gen/android/app/src/main/jniLibs/x86_64/libtauri_demo_lib.so new file mode 120000 index 0000000..0af9da1 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/jniLibs/x86_64/libtauri_demo_lib.so @@ -0,0 +1 @@ +/home/lenn/Workspace/JE-Skin/src-tauri/target/x86_64-linux-android/release/libtauri_demo_lib.so \ No newline at end of file diff --git a/src-tauri/gen/android/app/tauri.build.gradle.kts b/src-tauri/gen/android/app/tauri.build.gradle.kts new file mode 100644 index 0000000..5672388 --- /dev/null +++ b/src-tauri/gen/android/app/tauri.build.gradle.kts @@ -0,0 +1,6 @@ +// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +val implementation by configurations +dependencies { + implementation(project(":tauri-android")) + implementation(project(":tauri-plugin-opener")) +} \ No newline at end of file diff --git a/src-tauri/gen/android/app/tauri.properties b/src-tauri/gen/android/app/tauri.properties new file mode 100644 index 0000000..b0e9bcf --- /dev/null +++ b/src-tauri/gen/android/app/tauri.properties @@ -0,0 +1,3 @@ +// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +tauri.android.versionName=0.4.0 +tauri.android.versionCode=4000 \ No newline at end of file diff --git a/src-tauri/gen/android/tauri.settings.gradle b/src-tauri/gen/android/tauri.settings.gradle new file mode 100644 index 0000000..85634be --- /dev/null +++ b/src-tauri/gen/android/tauri.settings.gradle @@ -0,0 +1,5 @@ +// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +include ':tauri-android' +project(':tauri-android').projectDir = new File("/home/lenn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.10.3/mobile/android") +include ':tauri-plugin-opener' +project(':tauri-plugin-opener').projectDir = new File("/home/lenn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-opener-2.5.3/android") diff --git a/src/lib/components/CenterStage.svelte b/src/lib/components/CenterStage.svelte index de63228..49ddb5f 100644 --- a/src/lib/components/CenterStage.svelte +++ b/src/lib/components/CenterStage.svelte @@ -6,6 +6,7 @@ import { fly } from "svelte/transition"; import { DEFAULT_PRESSURE_RANGE_MAX, DEFAULT_PRESSURE_RANGE_MIN } from "$lib/config/pressure-range"; import ConfigPanel from "$lib/components/ConfigPanel.svelte"; + import ModelStage from "$lib/components/ModelStage.svelte"; import NeonBreakoutArena from "$lib/components/NeonBreakoutArena.svelte"; import PressureMatrixViewer from "$lib/components/PressureMatrixViewer.svelte"; import SignalChart from "$lib/components/SignalChart.svelte"; @@ -16,7 +17,8 @@ HudSummary, LocaleCode, MatrixDisplayMode, - PressureColorMapPreset + PressureColorMapPreset, + StageViewMode } from "$lib/types/hud"; export let locale: LocaleCode = "zh-CN"; @@ -41,6 +43,8 @@ export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX; export let colorMapPreset: PressureColorMapPreset = "emerald"; export let matrixDisplayMode: MatrixDisplayMode = "dots"; + export let stageViewMode: StageViewMode = "webgl"; + export let modelUrl = "/models/je-skin-model.glb"; export let replaySectionLabel = ""; export let replayPlayLabel = ""; export let replayPauseLabel = ""; @@ -84,6 +88,7 @@ $: summaryCurveVisible = summary.points.length > 0 && summary.points.some((value) => Number.isFinite(value) && Math.abs(value) >= 0.0001); $: splitMatrixTitle = locale === "zh-CN" ? "数字矩阵" : "Matrix"; $: splitMatrixHint = locale === "zh-CN" ? "实时压力数据 / 数字矩阵" : "Live pressure matrix"; + $: isModelStage = stageViewMode === "model3d"; function toPxNumber(rawValue: string): number { const value = Number.parseFloat(rawValue); @@ -176,7 +181,13 @@ bind:this={stagePlaneEl} style="--panel-zone-top-dyn: {panelZoneTopPx}px; --rail-scale-left: {leftRailScale}; --rail-scale-right: {rightRailScale};" > - {#if showPrecisionTestPanel} + {#if isModelStage} +
+ {#key modelUrl} + + {/key} +
+ {:else if showPrecisionTestPanel}
@@ -232,7 +243,7 @@
{/if} - {#if showConfigPanel && !showPrecisionTestPanel} + {#if showConfigPanel && !showPrecisionTestPanel && !isModelStage}
{/if} - {#if !showPrecisionTestPanel} + {#if !showPrecisionTestPanel && !isModelStage}