feat(android): add zoom slider
All checks were successful
/ build-web (push) Successful in 8m21s
/ build-desktop (linux) (push) Successful in 18m41s
/ build-mobile (push) Successful in 44m57s
/ release (push) Successful in 9m27s

fix(android): gray screen bug
This commit is contained in:
Alois 2026-08-11 15:50:52 +02:00
commit eaf3f61ebd
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
24 changed files with 456 additions and 372 deletions

View file

@ -14,7 +14,7 @@
}
},
"scripts": {
"dev:mobile:raw": "tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}",
"dev:mobile:raw": "adb reverse tcp:3000 tcp:3000 && tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}",
"start-adb:mobile:raw": "adb devices",
"build:mobile:raw": "tauri android build",
"build:mobile:ci": "node render-version.ts && trap 'node render-version.ts --unrender' EXIT && tauri android build --debug",

View file

@ -560,17 +560,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "cfb"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3"
dependencies = [
"fnv",
"uuid",
"web-time",
]
[[package]]
name = "cfg-expr"
version = "0.15.8"
@ -843,20 +832,7 @@ version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2"
dependencies = [
"cssparser-macros 0.6.1",
"dtoa-short",
"itoa",
"phf",
"smallvec",
]
[[package]]
name = "cssparser"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98"
dependencies = [
"cssparser-macros 0.7.0",
"cssparser-macros",
"dtoa-short",
"itoa",
"phf",
@ -873,16 +849,6 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "cssparser-macros"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a2a99df6e410a8ff4245aa2006499ea662245f967cc7c0a38c83ef8eb44dbf"
dependencies = [
"quote",
"syn 2.0.119",
]
[[package]]
name = "ctor"
version = "0.8.0"
@ -1179,26 +1145,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
dependencies = [
"bit-set",
"cssparser 0.36.0",
"cssparser",
"foldhash",
"html5ever 0.38.0",
"html5ever",
"precomputed-hash",
"selectors 0.36.1",
"tendril",
]
[[package]]
name = "dom_query"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc"
dependencies = [
"bit-set",
"cssparser 0.37.0",
"foldhash",
"html5ever 0.39.0",
"precomputed-hash",
"selectors 0.38.0",
"selectors",
"tendril",
]
@ -1383,7 +1334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -2073,17 +2024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
dependencies = [
"log",
"markup5ever 0.38.0",
]
[[package]]
name = "html5ever"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
dependencies = [
"log",
"markup5ever 0.39.0",
"markup5ever",
]
[[package]]
@ -2182,7 +2123,6 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
@ -2220,7 +2160,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.62.2",
"windows-core",
]
[[package]]
@ -2380,16 +2320,7 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
dependencies = [
"cfb 0.7.3",
]
[[package]]
name = "infer"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4"
dependencies = [
"cfb 0.14.0",
"cfb",
]
[[package]]
@ -2752,17 +2683,6 @@ dependencies = [
"web_atoms",
]
[[package]]
name = "markup5ever"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
dependencies = [
"log",
"tendril",
"web_atoms",
]
[[package]]
name = "memchr"
version = "2.8.3"
@ -3920,7 +3840,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -4100,13 +4020,14 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "reqwest"
version = "0.12.28"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
@ -4120,45 +4041,12 @@ dependencies = [
"quinn",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
]
[[package]]
name = "reqwest"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-util",
"tower",
"tower-http",
@ -4228,7 +4116,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -4287,7 +4175,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -4440,26 +4328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
dependencies = [
"bitflags 2.13.1",
"cssparser 0.36.0",
"derive_more",
"log",
"new_debug_unreachable",
"phf",
"phf_codegen",
"precomputed-hash",
"rustc-hash",
"servo_arc",
"smallvec",
]
[[package]]
name = "selectors"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d"
dependencies = [
"bitflags 2.13.1",
"cssparser 0.37.0",
"cssparser",
"derive_more",
"log",
"new_debug_unreachable",
@ -4588,18 +4457,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_with"
version = "3.21.0"
@ -5031,7 +4888,7 @@ dependencies = [
"unicode-segmentation",
"url",
"windows",
"windows-core 0.61.2",
"windows-core",
"windows-version",
"x11-dl",
]
@ -5082,13 +4939,13 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest 0.13.4",
"reqwest",
"serde",
"serde_json",
"serde_repr",
"serialize-to-javascript",
"swift-rs",
"tauri-build 2.6.3 (git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41)",
"tauri-build",
"tauri-macros",
"tauri-runtime",
"tauri-runtime-wry",
@ -5123,26 +4980,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-build"
version = "2.6.3"
source = "git+https://github.com/tauri-apps/tauri?rev=e2e585ad1196c9572f86ef39aae01ef4c3b1a762#e2e585ad1196c9572f86ef39aae01ef4c3b1a762"
dependencies = [
"anyhow",
"cargo_toml",
"dirs",
"glob",
"heck 0.5.0",
"json-patch 4.2.0",
"schemars 0.8.22",
"semver",
"serde",
"serde_json",
"tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?rev=e2e585ad1196c9572f86ef39aae01ef4c3b1a762)",
"tauri-winres",
"walkdir",
]
[[package]]
name = "tauri-codegen"
version = "2.6.3"
@ -5230,7 +5067,7 @@ dependencies = [
"tracing",
"url",
"windows-registry",
"windows-result 0.3.4",
"windows-result",
]
[[package]]
@ -5351,11 +5188,11 @@ dependencies = [
"anyhow",
"cargo_metadata",
"ctor 0.8.0",
"dom_query 0.27.0",
"dom_query",
"dunce",
"glob",
"http",
"infer 0.19.0",
"infer",
"json-patch 3.0.1",
"log",
"memchr",
@ -5387,11 +5224,11 @@ dependencies = [
"brotli",
"cargo_metadata",
"ctor 1.0.12",
"dom_query 0.27.0",
"dom_query",
"dunce",
"glob",
"http",
"infer 0.19.0",
"infer",
"json-patch 4.2.0",
"log",
"memchr",
@ -5415,42 +5252,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-utils"
version = "2.9.3"
source = "git+https://github.com/tauri-apps/tauri?rev=e2e585ad1196c9572f86ef39aae01ef4c3b1a762#e2e585ad1196c9572f86ef39aae01ef4c3b1a762"
dependencies = [
"anyhow",
"cargo_metadata",
"ctor 1.0.12",
"dom_query 0.28.0",
"dunce",
"glob",
"http",
"infer 0.22.0",
"json-patch 4.2.0",
"log",
"memchr",
"phf",
"plist",
"proc-macro2",
"quote",
"regex",
"schemars 0.8.22",
"semver",
"serde",
"serde-untagged",
"serde_json",
"serde_with",
"swift-rs",
"thiserror 2.0.19",
"toml 1.1.4+spec-1.1.0",
"url",
"urlpattern 0.6.0",
"uuid",
"walkdir",
]
[[package]]
name = "tauri-winres"
version = "0.3.6"
@ -5480,10 +5281,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -5497,17 +5298,17 @@ dependencies = [
[[package]]
name = "tensamin"
version = "0.0.0"
version = "0.0.11"
dependencies = [
"base64 0.22.1",
"jni 0.22.4",
"mtp",
"mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)",
"reqwest 0.12.28",
"reqwest",
"serde",
"serde_json",
"tauri",
"tauri-build 2.6.3 (git+https://github.com/tauri-apps/tauri?rev=e2e585ad1196c9572f86ef39aae01ef4c3b1a762)",
"tauri-build",
"tauri-plugin-barcode-scanner",
"tauri-plugin-deep-link",
"tauri-plugin-log",
@ -6265,15 +6066,6 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.2"
@ -6283,7 +6075,7 @@ dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows-core 0.61.2",
"windows-core",
"windows-implement",
"windows-interface",
]
@ -6307,7 +6099,7 @@ checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
dependencies = [
"thiserror 2.0.19",
"windows",
"windows-core 0.61.2",
"windows-core",
]
[[package]]
@ -6332,7 +6124,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -6363,7 +6155,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-core 0.61.2",
"windows-core",
"windows-future",
"windows-link 0.1.3",
"windows-numerics",
@ -6375,7 +6167,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core 0.61.2",
"windows-core",
]
[[package]]
@ -6387,21 +6179,8 @@ dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
"windows-result",
"windows-strings",
]
[[package]]
@ -6410,7 +6189,7 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-core",
"windows-link 0.1.3",
"windows-threading",
]
@ -6455,7 +6234,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.2",
"windows-core",
"windows-link 0.1.3",
]
@ -6466,8 +6245,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
"windows-result",
"windows-strings",
]
[[package]]
@ -6479,15 +6258,6 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
@ -6497,15 +6267,6 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@ -6732,7 +6493,7 @@ dependencies = [
"cookie",
"crossbeam-channel",
"dirs",
"dom_query 0.27.0",
"dom_query",
"dpi",
"dunce",
"gdkx11",
@ -6760,7 +6521,7 @@ dependencies = [
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows-core 0.61.2",
"windows-core",
"windows-version",
"x11-dl",
]

View file

@ -15,14 +15,14 @@ name = "mobile_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { git = "https://github.com/tauri-apps/tauri", rev = "e2e585ad1196c9572f86ef39aae01ef4c3b1a762", features = [] }
tauri-build = { git = "https://github.com/tauri-apps/tauri", rev = "4af26a3f7f8b692d62cca549bbacd93f5ce90b41", features = [] }
[dependencies]
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
base64 = "0.22"
reqwest = { version = "0.13", default-features = false, features = ["json", "rustls-tls"] }
reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] }
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
mtp = { git = "https://git.methanium.net/methanium/mtp.git", rev = "7182272e3edb0079d7b74b45b2f62a298d2fee22", features = ["client", "crypto"] }
mtp-transport = { git = "https://git.methanium.net/methanium/mtp.git", rev = "b067614a684eb1856bc5db7b3fd82148c036ce6b" }

View file

@ -1,13 +1,9 @@
{
"identifier": "mobile-capability",
"platforms": [
"android",
"iOS"
],
"windows": [
"main"
],
"platforms": ["android", "iOS"],
"windows": ["main"],
"permissions": [
"core:event:default",
"deep-link:default",
"barcode-scanner:default",
"barcode-scanner:allow-scan",

View file

@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
import java.io.FileInputStream
@ -61,14 +62,17 @@ android {
)
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}
rust {
rootDirRel = "../../../"
}

View file

@ -3,10 +3,8 @@ package net.tensamin.client
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.graphics.Rect
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.WindowManager
import android.webkit.JavascriptInterface
@ -56,7 +54,7 @@ class MainActivity : TauriActivity() {
}
override fun onWebViewCreate(webView: WebView) {
webView.setInitialScale(290)
NativeAccessibilityBridge.attach(webView)
mediaWebView = webView
MobileMediaEvents.attach(webView)
webView.addJavascriptInterface(MobileMediaJavascriptInterface(), "tensaminMobileMedia")
@ -65,11 +63,12 @@ class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, true)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
super.onCreate(savedInstanceState)
NativeMtpBridge.nativeAttach(applicationContext)
if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) {
NativeMtpBridge.startService(this)
}
super.onCreate(savedInstanceState)
NativeMtpBridge.nativeAttach(applicationContext)
NativeAccessibilityBridge.nativeAttach(applicationContext)
installKeyboardResizeWorkaround()
}
@ -90,7 +89,10 @@ class MainActivity : TauriActivity() {
attachLayoutListener = null
contentRoot = null
contentChild = null
mediaWebView?.removeJavascriptInterface("tensaminMobileMedia")
mediaWebView?.let {
NativeAccessibilityBridge.detach(it)
it.removeJavascriptInterface("tensaminMobileMedia")
}
mediaWebView = null
MobileMediaEvents.detach()
super.onDestroy()
@ -203,16 +205,20 @@ class MainActivity : TauriActivity() {
child: android.view.View,
insets: WindowInsetsCompat? = ViewCompat.getRootWindowInsets(child),
) {
val visibleFrame = Rect()
child.getWindowVisibleDisplayFrame(visibleFrame)
val rootHeight = child.rootView.height
if (rootHeight <= 0) return
val imeHeight = insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
val keyboardHeight = maxOf(imeHeight, rootHeight - visibleFrame.bottom)
val keyboardVisible = keyboardHeight > rootHeight * 0.15
val usableHeight = if (keyboardVisible) rootHeight - keyboardHeight else ViewGroup.LayoutParams.MATCH_PARENT
val imeVisible = insets?.isVisible(WindowInsetsCompat.Type.ime()) == true
val imeHeight = if (imeVisible) {
insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
} else {
0
}
val usableHeight = if (imeHeight in 1 until rootHeight) {
rootHeight - imeHeight
} else {
WindowManager.LayoutParams.MATCH_PARENT
}
if (previousUsableHeight == usableHeight) return

View file

@ -26,6 +26,7 @@ import android.os.HandlerThread
import android.os.IBinder
import android.util.Base64
import android.util.DisplayMetrics
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import java.io.ByteArrayOutputStream
import java.util.concurrent.atomic.AtomicBoolean
@ -107,16 +108,12 @@ class MediaProjectionService : Service() {
stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
android.app.Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
} else {
android.app.Notification.Builder(this)
}
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_menu_share)
.setContentTitle("Tensamin is sharing your screen")
.setContentText("Tap Stop to end screen sharing")
.setOngoing(true)
.setCategory(android.app.Notification.CATEGORY_SERVICE)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.addAction(android.R.drawable.ic_media_pause, "Stop", stopPendingIntent)
.build()

View file

@ -13,6 +13,8 @@ import android.net.Network
import android.net.NetworkCapabilities
import android.os.Build
import android.os.IBinder
import android.os.Process
import android.os.SystemClock
import androidx.core.app.NotificationCompat
class MtpForegroundService : Service() {
@ -74,10 +76,19 @@ class MtpForegroundService : Service() {
}
override fun onTaskRemoved(rootIntent: Intent?) {
val preferences = getSharedPreferences(SERVICE_PREFERENCES, Context.MODE_PRIVATE)
val now = SystemClock.elapsedRealtime()
if (now - preferences.getLong(LAST_TASK_RESTART, 0) < TASK_RESTART_COOLDOWN_MS) {
super.onTaskRemoved(rootIntent)
return
}
preferences.edit().putLong(LAST_TASK_RESTART, now).commit()
if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) {
startService(Intent(this, MtpForegroundService::class.java))
}
super.onTaskRemoved(rootIntent)
// Tauri cannot recreate its WebView after the UI task is removed while this process survives.
Process.killProcess(Process.myPid())
}
override fun onDestroy() {
@ -94,6 +105,9 @@ class MtpForegroundService : Service() {
private const val CHANNEL_ID = "tensamin-connection"
private const val NOTIFICATION_ID = 2201
private const val ACTION_STOP = "net.tensamin.client.STOP_MTP"
private const val SERVICE_PREFERENCES = "tensamin-service"
private const val LAST_TASK_RESTART = "last-task-restart"
private const val TASK_RESTART_COOLDOWN_MS = 15_000L
@Volatile private var connectionStatus = "Connecting"
fun updateNotification(context: Context, status: String) {

View file

@ -0,0 +1,51 @@
package net.tensamin.client
import android.content.Context
import android.webkit.WebView
import androidx.annotation.Keep
import java.lang.ref.WeakReference
@Keep
object NativeAccessibilityBridge {
const val DEFAULT_INITIAL_SCALE = 290
private const val MIN_INITIAL_SCALE = 210
private const val MAX_INITIAL_SCALE = 500
private const val PREFERENCES = "tensamin-accessibility"
private const val INITIAL_SCALE = "initial-scale"
private var webView = WeakReference<WebView>(null)
init {
System.loadLibrary("mobile_lib")
}
@JvmStatic external fun nativeAttach(context: Context)
fun attach(webView: WebView) {
this.webView = WeakReference(webView)
webView.setInitialScale(getInitialScale(webView.context))
}
fun detach(webView: WebView) {
if (this.webView.get() === webView) this.webView.clear()
}
fun getInitialScale(context: Context): Int {
val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
val storedScale = preferences.getInt(INITIAL_SCALE, DEFAULT_INITIAL_SCALE)
val scale = storedScale.coerceIn(MIN_INITIAL_SCALE, MAX_INITIAL_SCALE)
if (scale != storedScale) preferences.edit().putInt(INITIAL_SCALE, scale).apply()
return scale
}
fun setInitialScale(context: Context, initialScale: Int) {
val nextScale = initialScale.coerceIn(MIN_INITIAL_SCALE, MAX_INITIAL_SCALE)
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.edit()
.putInt(INITIAL_SCALE, nextScale)
.apply()
webView.get()?.let { currentWebView ->
currentWebView.post { currentWebView.setInitialScale(nextScale) }
}
}
}

View file

@ -1,4 +1,4 @@
import com.android.build.gradle.LibraryExtension
import com.android.build.api.dsl.LibraryExtension
buildscript {
repositories {
@ -6,8 +6,8 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:9.3.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.10")
classpath("com.android.tools.build:gradle:8.11.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20")
}
}

View file

@ -18,6 +18,5 @@ repositories {
dependencies {
compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:9.3.1")
implementation("com.android.tools.build:gradle:8.11.0")
}

View file

@ -5,8 +5,12 @@ import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import javax.inject.Inject
open class BuildTask : DefaultTask() {
open class BuildTask @Inject constructor(
private val execOperations: ExecOperations,
) : DefaultTask() {
@Input
var rootDirRel: String? = null
@Input
@ -50,7 +54,7 @@ open class BuildTask : DefaultTask() {
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("tauri", "android", "android-studio-script");
project.exec {
execOperations.exec {
workingDir(File(project.projectDir, rootDirRel))
executable(executable)
args(args)
@ -65,4 +69,4 @@ open class BuildTask : DefaultTask() {
args(listOf("--target", target))
}.assertNormalExitValue()
}
}
}

View file

@ -0,0 +1,135 @@
#[cfg(not(target_os = "android"))]
const DEFAULT_INITIAL_SCALE: i32 = 290;
const MIN_INITIAL_SCALE: i32 = 210;
const MAX_INITIAL_SCALE: i32 = 500;
#[tauri::command]
pub fn accessibility_get_initial_scale() -> Result<i32, String> {
android_get_initial_scale()
}
#[tauri::command]
pub fn accessibility_set_initial_scale(initial_scale: i32) -> Result<(), String> {
if !(MIN_INITIAL_SCALE..=MAX_INITIAL_SCALE).contains(&initial_scale) {
return Err(format!(
"Initial scale must be between {MIN_INITIAL_SCALE} and {MAX_INITIAL_SCALE}"
));
}
android_set_initial_scale(initial_scale)
}
#[cfg(not(target_os = "android"))]
fn android_get_initial_scale() -> Result<i32, String> {
Ok(DEFAULT_INITIAL_SCALE)
}
#[cfg(not(target_os = "android"))]
fn android_set_initial_scale(_: i32) -> Result<(), String> {
Ok(())
}
#[cfg(target_os = "android")]
mod android {
use std::sync::OnceLock;
use jni::{
jni_sig, jni_str,
objects::{Global, JClass, JObject, JValue},
Env, EnvUnowned, JavaVM,
};
struct Host {
vm: JavaVM,
context: Global<JObject<'static>>,
bridge: Global<JObject<'static>>,
}
static HOST: OnceLock<Host> = OnceLock::new();
fn attach(env: &mut Env, context: JObject) -> Result<(), String> {
if HOST.get().is_some() {
return Ok(());
}
let class = env
.find_class(jni_str!("net/tensamin/client/NativeAccessibilityBridge"))
.map_err(|error| error.to_string())?;
let bridge = env
.get_static_field(
class,
jni_str!("INSTANCE"),
jni_sig!("Lnet/tensamin/client/NativeAccessibilityBridge;"),
)
.and_then(|value| value.l())
.map_err(|error| error.to_string())?;
HOST.set(Host {
vm: env.get_java_vm().map_err(|error| error.to_string())?,
context: env
.new_global_ref(context)
.map_err(|error| error.to_string())?,
bridge: env
.new_global_ref(bridge)
.map_err(|error| error.to_string())?,
})
.map_err(|_| "Android accessibility host is already attached".to_string())
}
fn with_env<T>(call: impl FnOnce(&mut Env, &Host) -> Result<T, String>) -> Result<T, String> {
let host = HOST
.get()
.ok_or("Android accessibility host is not attached")?;
host.vm
.attach_current_thread(|env| Ok::<_, jni::errors::Error>(call(env, host)))
.map_err(|error| error.to_string())?
}
pub fn get_initial_scale() -> Result<i32, String> {
with_env(|env, host| {
env.call_method(
host.bridge.as_obj(),
jni_str!("getInitialScale"),
jni_sig!("(Landroid/content/Context;)I"),
&[JValue::Object(host.context.as_obj())],
)
.and_then(|value| value.i())
.map_err(|error| error.to_string())
})
}
pub fn set_initial_scale(initial_scale: i32) -> Result<(), String> {
with_env(|env, host| {
env.call_method(
host.bridge.as_obj(),
jni_str!("setInitialScale"),
jni_sig!("(Landroid/content/Context;I)V"),
&[
JValue::Object(host.context.as_obj()),
JValue::Int(initial_scale),
],
)
.map_err(|error| error.to_string())?;
Ok(())
})
}
#[no_mangle]
pub extern "system" fn Java_net_tensamin_client_NativeAccessibilityBridge_nativeAttach<
'caller,
>(
mut env: EnvUnowned<'caller>,
_class: JClass,
context: JObject<'caller>,
) {
let _ = env.with_env(|env| {
let _ = attach(env, context);
Ok::<_, jni::errors::Error>(())
});
}
}
#[cfg(target_os = "android")]
use android::{
get_initial_scale as android_get_initial_scale, set_initial_scale as android_set_initial_scale,
};

View file

@ -1,3 +1,4 @@
mod accessibility_backend;
mod mtp_backend;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@ -19,6 +20,8 @@ pub fn run() {
let app = builder
.invoke_handler(tauri::generate_handler![
accessibility_backend::accessibility_get_initial_scale,
accessibility_backend::accessibility_set_initial_scale,
mtp_backend::mtp_request,
mtp_backend::mtp_status,
mtp_backend::mtp_store_credentials,

View file

@ -372,9 +372,13 @@ async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> {
.with_max_missed_pings(3);
#[cfg(target_os = "android")]
let client_config = client_config.with_pinned_pem(android_root_certificates().clone());
let connection = MTPClient::auth_connect(client_config, &keyring, &host_key)
.await
.map_err(|error| format!("transport authentication failed: {error}"))?;
let connection = tokio::time::timeout(
Duration::from_secs(30),
MTPClient::auth_connect(client_config, &keyring, &host_key),
)
.await
.map_err(|_| "transport authentication timed out".to_string())?
.map_err(|error| format!("transport authentication failed: {error}"))?;
manager().log(2, "Native MTP authentication completed", None);
let connected = CommunicationValue::new(CommunicationType::ClientConnected)
@ -385,10 +389,13 @@ async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> {
.add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0))
.add_typed_default(DataType::CacheValid, DataValue::BoolFalse)
.add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0));
let state = connection
.request(&connected, None)
.await
.map_err(|error| format!("initial state synchronization failed: {error}"))?;
let state = tokio::time::timeout(
Duration::from_secs(30),
connection.request(&connected, None),
)
.await
.map_err(|_| "initial state synchronization timed out".to_string())?
.map_err(|error| format!("initial state synchronization failed: {error}"))?;
if !state.is_type(CommunicationType::ClientStateSync) {
return Err(format!(
"expected ClientStateSync, received {}",
@ -406,9 +413,9 @@ async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> {
let ack = CommunicationValue::new(CommunicationType::ClientStateAck)
.add_typed_default(DataType::SessionId, number_to_data(session_id))
.add_typed_default(DataType::VersionNumber, number_to_data(version));
let response = connection
.request(&ack, None)
let response = tokio::time::timeout(Duration::from_secs(30), connection.request(&ack, None))
.await
.map_err(|_| "state acknowledgement timed out".to_string())?
.map_err(|error| format!("state acknowledgement failed: {error}"))?;
if response
.get_type_name()
@ -433,7 +440,19 @@ async fn resolve_endpoint(config: &MtpConfig) -> Result<(String, String), String
public_key: String,
}
let root = config.omega_url.trim_end_matches('/');
let response = reqwest::get(format!("{root}/api/get/omikron/{}", config.user_id))
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(20));
#[cfg(target_os = "android")]
let client = client.tls_certs_only(
reqwest::Certificate::from_pem_bundle(android_root_certificates())
.map_err(|error| format!("invalid bundled root certificates: {error}"))?,
);
let response = client
.build()
.map_err(|error| error.to_string())?
.get(format!("{root}/api/get/omikron/{}", config.user_id))
.send()
.await
.map_err(|error| error.to_string())?;
if !response.status().is_success() {

View file

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<link rel="icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tensamin</title>
</head>

View file

@ -6,7 +6,7 @@
"scripts": {
"format": "pnpm exec prettier --write .",
"lint": "eslint src",
"dev": "vite --port 3000 --host 0.0.0.0",
"dev": "vite",
"test": "vitest run --passWithNoTests",
"build": "pnpm run test && tsc -b && vite build",
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."

View file

@ -76,11 +76,6 @@ function parseTuFileContent(rawFileContent: string): {
throw new Error("Invalid file");
}
console.log({
domain,
userId,
});
return { userId, privateKey, domain };
}

View file

@ -70,9 +70,9 @@ export default defineConfig({
],
},
server: {
port: 5173,
port: 3000,
strictPort: true,
host: host || "0.0.0.0",
host: "0.0.0.0",
hmr: host
? {
protocol: "ws",