diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..46adaad --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +MTP_TYPE_MAPS = { value = "mtp-type-maps/type-maps.yaml", relative = true } diff --git a/.fallowrc.json b/.fallowrc.json index 022e0cc..ba50fb5 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -3,6 +3,7 @@ "entry": [ "src/index.{ts,tsx,js,jsx}", "src/main.{ts,tsx,js,jsx}", + "apps/pwa/src/serviceWorker.ts", "apps/tauri/render-version.ts", "packages/**/*.test.ts" ], diff --git a/.forgejo/workflows/dependency-builds.yml b/.forgejo/workflows/dependency-builds.yml new file mode 100644 index 0000000..2ae48fe --- /dev/null +++ b/.forgejo/workflows/dependency-builds.yml @@ -0,0 +1,60 @@ +name: Dependency builds + +on: + pull_request: + +env: + FORGEJO_TOKEN: "" + GITHUB_TOKEN: "" + +jobs: + web: + if: ${{ github.actor == 'rasensprenger' }} + name: Build web + runs-on: nixos + steps: + - uses: https://data.forgejo.org/actions/checkout@v4 + with: + persist-credentials: false + - run: git submodule update --init --recursive + - run: nix develop .#electron --command pnpm install --frozen-lockfile + - run: nix develop .#electron --command pnpm run build:packages + - run: nix develop .#electron --command pnpm run build:web + + desktop: + if: ${{ github.actor == 'rasensprenger' }} + name: Build desktop + runs-on: nixos + steps: + - uses: https://data.forgejo.org/actions/checkout@v4 + with: + persist-credentials: false + - run: git submodule update --init --recursive + - run: nix develop .#electron --command pnpm install --frozen-lockfile + - run: nix develop .#electron --command pnpm run build:packages + - run: nix develop .#electron --command pnpm run build:desktop + + native-mtp: + if: ${{ github.actor == 'rasensprenger' }} + name: Test native MTP + runs-on: nixos + steps: + - run: nix profile add nixpkgs#nodejs_24 + - uses: https://data.forgejo.org/actions/checkout@v4 + with: + persist-credentials: false + - run: git submodule update --init --recursive + - run: nix develop .#electron --command bash -lc 'cd apps/tauri/src-tauri && cargo test' + + mobile: + if: ${{ github.actor == 'rasensprenger' }} + name: Build mobile + runs-on: nixos + steps: + - uses: https://data.forgejo.org/actions/checkout@v4 + with: + persist-credentials: false + - run: git submodule update --init --recursive + - run: nix develop .#tauri --command pnpm install --frozen-lockfile + - run: nix develop .#tauri --command pnpm run build:packages + - run: nix develop .#tauri --command pnpm --dir apps/tauri run build:mobile:ci diff --git a/.forgejo/workflows/deploy-dev.yml b/.forgejo/workflows/deploy-dev.yml index 0d5be50..7f59ec1 100644 --- a/.forgejo/workflows/deploy-dev.yml +++ b/.forgejo/workflows/deploy-dev.yml @@ -1,20 +1,15 @@ on: + workflow_dispatch: push: branches: - dev paths-ignore: - flake.nix -env: - NIX_CONFIG: experimental-features = nix-command flakes - jobs: build-web: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -39,9 +34,6 @@ jobs: build-mobile: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -62,8 +54,6 @@ jobs: KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }} run: | - nix profile add nixpkgs#gnused - set -euo pipefail if [ -z "$KEYSTORE_BASE64" ]; then @@ -128,9 +118,6 @@ jobs: matrix: target: [linux] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -181,9 +168,6 @@ jobs: runs-on: nixos needs: [build-web, build-mobile, build-desktop] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 with: diff --git a/.forgejo/workflows/deploy-prod.yml b/.forgejo/workflows/deploy-prod.yml index d85ce27..621a0cd 100644 --- a/.forgejo/workflows/deploy-prod.yml +++ b/.forgejo/workflows/deploy-prod.yml @@ -6,16 +6,10 @@ on: paths-ignore: - flake.nix -env: - NIX_CONFIG: experimental-features = nix-command flakes - jobs: build-web: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -40,9 +34,6 @@ jobs: build-mobile: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -63,8 +54,6 @@ jobs: KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }} run: | - nix profile add nixpkgs#gnused - set -euo pipefail if [ -z "$KEYSTORE_BASE64" ]; then @@ -129,9 +118,6 @@ jobs: matrix: target: [linux] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -180,9 +166,6 @@ jobs: runs-on: nixos needs: [build-web, build-mobile, build-desktop] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -328,58 +311,3 @@ jobs: "$API/repos/$REPO/releases/$release_id" done < "$DELETE_RELEASES" EOF - - - name: Update root flake release hash - env: - TAG: ${{ steps.version.outputs.tag }} - run: | - nix develop .#electron --command bash <<'EOF' - set -eu - - DEB="$(find releases -maxdepth 1 -type f -name 'Tensamin-*-linux-amd64.deb' -print -quit)" - test -n "$DEB" - - HASH="$(node -e 'const fs = require("fs"); const crypto = require("crypto"); const file = process.argv[1]; console.log("sha256-" + crypto.createHash("sha256").update(fs.readFileSync(file)).digest("base64"));' "$DEB")" - export HASH - - node -e ' - const fs = require("fs"); - const version = process.env.TAG; - const hash = process.env.HASH; - let content = fs.readFileSync("flake.nix", "utf8"); - content = content.replace(/version = "[^"]+";/, `version = "${version}";`); - content = content.replace(/x86_64DebHash = "sha256-[^"]+";/, `x86_64DebHash = "${hash}";`); - fs.writeFileSync("flake.nix", content); - ' - - if git diff --quiet -- flake.nix; then - echo "flake.nix already has the current release hash on main." - else - git add flake.nix - git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash" - git push - fi - - git fetch origin dev - git worktree add ../dev-flake-update origin/dev - cd ../dev-flake-update - - node -e ' - const fs = require("fs"); - const version = process.env.TAG; - const hash = process.env.HASH; - let content = fs.readFileSync("flake.nix", "utf8"); - content = content.replace(/version = "[^"]+";/, `version = "${version}";`); - content = content.replace(/x86_64DebHash = "sha256-[^"]+";/, `x86_64DebHash = "${hash}";`); - fs.writeFileSync("flake.nix", content); - ' - - if git diff --quiet -- flake.nix; then - echo "flake.nix already has the current release hash on dev." - exit 0 - fi - - git add flake.nix - git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash" - git push origin HEAD:dev - EOF diff --git a/.gitignore b/.gitignore index c0e6a9b..261f60b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules releases .fallow .direnv +keystore.jks +keystore.properties diff --git a/TODO b/TODO index 3ec0948..e4b9435 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,4 @@ - Add a bunch of tests -- Add packages/hotkeys/ - Full accessability - Add settings saving & update onboarding to use it - Add onboarding page to load one profile during onboarding diff --git a/apps/electron/build/icons/128x128.png b/apps/electron/build/icons/128x128.png deleted file mode 100644 index 357e1c2..0000000 Binary files a/apps/electron/build/icons/128x128.png and /dev/null differ diff --git a/apps/electron/build/icons/128x128@2x.png b/apps/electron/build/icons/128x128@2x.png deleted file mode 100644 index be55e24..0000000 Binary files a/apps/electron/build/icons/128x128@2x.png and /dev/null differ diff --git a/apps/electron/build/icons/32x32.png b/apps/electron/build/icons/32x32.png deleted file mode 100644 index bc4fba4..0000000 Binary files a/apps/electron/build/icons/32x32.png and /dev/null differ diff --git a/apps/electron/build/icons/64x64.png b/apps/electron/build/icons/64x64.png deleted file mode 100644 index 567f4ad..0000000 Binary files a/apps/electron/build/icons/64x64.png and /dev/null differ diff --git a/apps/electron/build/icons/icon.icns b/apps/electron/build/icons/icon.icns deleted file mode 100644 index 7f3b591..0000000 Binary files a/apps/electron/build/icons/icon.icns and /dev/null differ diff --git a/apps/electron/build/icons/icon.ico b/apps/electron/build/icons/icon.ico deleted file mode 100644 index c5c68f8..0000000 Binary files a/apps/electron/build/icons/icon.ico and /dev/null differ diff --git a/apps/electron/build/icons/icon.png b/apps/electron/build/icons/icon.png deleted file mode 100644 index dad3edb..0000000 Binary files a/apps/electron/build/icons/icon.png and /dev/null differ diff --git a/apps/electron/package.json b/apps/electron/package.json index f410033..560ed34 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -28,12 +28,11 @@ "validate:raw": "pnpm run build && pnpm run package:linux:raw", "validate": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run validate:raw; else pnpm run validate:raw; fi" }, - "dependencies": {}, "devDependencies": { - "@types/node": "^25.9.1", - "electron": "^39.2.7", - "electron-builder": "^26.0.12", - "esbuild": "^0.25.11", + "@types/node": "^26.1.2", + "electron": "^43.3.0", + "electron-builder": "^26.15.3", + "esbuild": "^0.28.1", "typescript": "~6.0.3" }, "build": { @@ -95,7 +94,11 @@ "target": [ "dmg" ], - "icon": "build/icons/icon.icns" + "icon": "build/icons/icon.icns", + "extendInfo": { + "NSCameraUsageDescription": "Tensamin uses your camera when you choose to share it in a call.", + "NSMicrophoneUsageDescription": "Tensamin uses your microphone for calls." + } }, "publish": null } diff --git a/apps/electron/src/main/main.ts b/apps/electron/src/main/main.ts index a5843e0..083d4d4 100644 --- a/apps/electron/src/main/main.ts +++ b/apps/electron/src/main/main.ts @@ -5,6 +5,7 @@ import { app, BrowserWindow, desktopCapturer, + globalShortcut, ipcMain, session, shell, @@ -13,6 +14,7 @@ import { checkForUpdates } from "./updates.js"; import { ipcChannels, type DesktopCallStatus, + type DesktopGlobalHotkeyBinding, type DesktopScreenShareAudioOutput, type DesktopScreenShareCapabilities, } from "../shared/ipc.js"; @@ -29,6 +31,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const verbose = process.argv.includes("--verbose"); let mainWindow: BrowserWindow | null = null; let selectedScreenShareSourceId: string | null = null; +let globalHotkeyBindings: DesktopGlobalHotkeyBinding[] = []; +let globalHotkeysSuspended = false; app.setName("tensamin"); app.setPath("userData", join(app.getPath("appData"), "tensamin", "electron")); @@ -46,6 +50,13 @@ if ( app.commandLine.appendSwitch("password-store", "gnome-libsecret"); } +if ( + process.platform === "linux" && + process.env.XDG_SESSION_TYPE === "wayland" +) { + app.commandLine.appendSwitch("enable-features", "GlobalShortcutsPortal"); +} + if ( process.platform === "linux" && process.env.XDG_SESSION_TYPE === "wayland" && @@ -180,6 +191,29 @@ function registerDisplayMediaHandler() { ); } +function registerMediaPermissionHandler() { + const isTrustedRenderer = (url: string) => { + try { + const parsed = new URL(url); + return parsed.protocol === "file:"; + } catch { + return false; + } + }; + + session.defaultSession.setPermissionCheckHandler( + (_webContents, permission, requestingOrigin) => + permission === "media" && isTrustedRenderer(requestingOrigin), + ); + session.defaultSession.setPermissionRequestHandler( + (_webContents, permission, callback, details) => { + callback( + permission === "media" && isTrustedRenderer(details.requestingUrl), + ); + }, + ); +} + function registerIpc() { verboseLog("registering ipc handlers"); @@ -219,6 +253,24 @@ function registerIpc() { deleteSecureStorage(key), ); ipcMain.handle(ipcChannels.clearSecureStorage, clearSecureStorage); + ipcMain.handle( + ipcChannels.setGlobalHotkeyBindings, + (event, bindings: unknown) => { + assertTrustedRenderer(event); + return setGlobalHotkeyBindings(bindings); + }, + ); + ipcMain.handle( + ipcChannels.setGlobalHotkeysSuspended, + (event, suspended: unknown) => { + assertTrustedRenderer(event); + if (typeof suspended !== "boolean") { + throw new Error("Invalid hotkey suspension state."); + } + globalHotkeysSuspended = suspended; + return applyGlobalHotkeyBindings(); + }, + ); ipcMain.handle(ipcChannels.setCallStatus, (_event, status: unknown) => { if ( typeof status !== "object" || @@ -259,6 +311,90 @@ function registerIpc() { }); } +function assertTrustedRenderer(event: Electron.IpcMainInvokeEvent) { + const target = mainWindow; + if ( + !target || + target.isDestroyed() || + event.sender !== target.webContents || + event.senderFrame !== target.webContents.mainFrame + ) { + throw new Error("Untrusted hotkey IPC sender."); + } + + try { + if (fileURLToPath(event.senderFrame.url) === getRendererIndex()) return; + } catch { + // Fall through to the rejection below. + } + throw new Error("Untrusted hotkey IPC sender."); +} + +function validGlobalHotkeyBindings( + value: unknown, +): value is DesktopGlobalHotkeyBinding[] { + return ( + Array.isArray(value) && + value.length <= 64 && + value.every( + (binding) => + binding && + typeof binding === "object" && + typeof (binding as DesktopGlobalHotkeyBinding).id === "string" && + /^[a-z0-9.-]+$/i.test((binding as DesktopGlobalHotkeyBinding).id) && + (binding as DesktopGlobalHotkeyBinding).id.length > 0 && + (binding as DesktopGlobalHotkeyBinding).id.length <= 128 && + typeof (binding as DesktopGlobalHotkeyBinding).accelerator === + "string" && + (binding as DesktopGlobalHotkeyBinding).accelerator.length > 0 && + (binding as DesktopGlobalHotkeyBinding).accelerator.length <= 128, + ) + ); +} + +function applyGlobalHotkeyBindings() { + globalShortcut.unregisterAll(); + const statuses = Object.fromEntries( + globalHotkeyBindings.map(({ id }) => [id, false]), + ); + if (globalHotkeysSuspended) return statuses; + + const grouped = new Map(); + for (const { id, accelerator } of globalHotkeyBindings) { + const ids = grouped.get(accelerator) ?? []; + ids.push(id); + grouped.set(accelerator, ids); + } + + for (const [accelerator, ids] of grouped) { + let registered = false; + try { + registered = globalShortcut.register(accelerator, () => { + const target = mainWindow; + if (!target || target.isDestroyed()) return; + ids.forEach((id) => + target.webContents.send(ipcChannels.globalHotkeyTriggered, id), + ); + }); + } catch (error) { + console.error("Failed to register global hotkey", accelerator, error); + } + ids.forEach((id) => { + statuses[id] = registered; + }); + } + + return statuses; +} + +function setGlobalHotkeyBindings(bindings: unknown) { + if (!validGlobalHotkeyBindings(bindings)) { + throw new Error("Invalid global hotkey bindings."); + } + globalHotkeyBindings = bindings; + return applyGlobalHotkeyBindings(); +} + async function createWindow() { const rendererIndex = getRendererIndex(); verboseLog("creating main window", { @@ -344,6 +480,10 @@ app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) void createWindow(); }); +app.on("will-quit", () => { + globalShortcut.unregisterAll(); +}); + if (verbose) { process.on("uncaughtException", (error) => { console.error("[tensamin:electron] uncaught exception", error); @@ -360,6 +500,7 @@ async function start() { verboseLog("app ready"); registerIpc(); registerDisplayMediaHandler(); + registerMediaPermissionHandler(); initTray(() => mainWindow); await createWindow(); } diff --git a/apps/electron/src/preload/preload.ts b/apps/electron/src/preload/preload.ts index c6b61e4..9f1c932 100644 --- a/apps/electron/src/preload/preload.ts +++ b/apps/electron/src/preload/preload.ts @@ -2,6 +2,7 @@ import { contextBridge, ipcRenderer } from "electron"; import { ipcChannels, type DesktopCallStatus, + type DesktopGlobalHotkeyBinding, type DesktopScreenShareSource, secureStorageLimits, } from "../shared/ipc.js"; @@ -55,6 +56,23 @@ const desktopApi = { return ipcRenderer.invoke(ipcChannels.setCallStatus, status); }, }, + hotkeys: { + setBindings: (bindings: DesktopGlobalHotkeyBinding[]) => + ipcRenderer.invoke(ipcChannels.setGlobalHotkeyBindings, bindings), + setSuspended: (suspended: boolean) => + typeof suspended === "boolean" + ? ipcRenderer.invoke(ipcChannels.setGlobalHotkeysSuspended, suspended) + : Promise.reject(new Error("Invalid hotkey suspension state.")), + onTriggered: (callback: (id: string) => void) => { + const listener = (_event: Electron.IpcRendererEvent, id: unknown) => { + if (typeof id === "string") callback(id); + }; + ipcRenderer.on(ipcChannels.globalHotkeyTriggered, listener); + return () => { + ipcRenderer.removeListener(ipcChannels.globalHotkeyTriggered, listener); + }; + }, + }, secureStorage: { getStatus: () => ipcRenderer.invoke(ipcChannels.getSecureStorageStatus), load: (key: string) => diff --git a/apps/electron/src/shared/ipc.ts b/apps/electron/src/shared/ipc.ts index 0679349..c424265 100644 --- a/apps/electron/src/shared/ipc.ts +++ b/apps/electron/src/shared/ipc.ts @@ -31,6 +31,11 @@ export type DesktopSecureStorageStatus = { backend: string | null; }; +export type DesktopGlobalHotkeyBinding = { + id: string; + accelerator: string; +}; + export const secureStorageLimits = { maxKeyBytes: 256, maxValueBytes: 1024 * 1024, @@ -77,4 +82,7 @@ export const ipcChannels = { saveSecureStorage: "secureStorage:save", deleteSecureStorage: "secureStorage:delete", clearSecureStorage: "secureStorage:clear", + setGlobalHotkeyBindings: "hotkeys:setBindings", + setGlobalHotkeysSuspended: "hotkeys:setSuspended", + globalHotkeyTriggered: "hotkeys:triggered", } as const; diff --git a/apps/pwa/package.json b/apps/pwa/package.json new file mode 100644 index 0000000..39c4ec5 --- /dev/null +++ b/apps/pwa/package.json @@ -0,0 +1,36 @@ +{ + "name": "@tensamin/pwa", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + "./vite": "./src/vite.ts", + "./runtime": "./src/runtime.tsx" + }, + "scripts": { + "format": "pnpm exec prettier --write .", + "lint": "eslint src", + "build": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.worker.json --noEmit" + }, + "dependencies": { + "@methanium/ui": "*", + "@tauri-apps/api": "^2.11.1", + "@tensamin/crypto": "workspace:*", + "@tensamin/shared": "workspace:*", + "@tensamin/storage": "workspace:*", + "mtp": "*", + "react": "^19.2.8", + "sonner": "^2.0.7", + "vite-plugin-pwa": "^1.1.0", + "workbox-core": "^7.3.0", + "workbox-precaching": "^7.3.0", + "workbox-routing": "^7.3.0", + "workbox-strategies": "^7.3.0" + }, + "devDependencies": { + "@types/node": "^26.1.2", + "@types/react": "^19.2.18", + "typescript": "~6.0.3", + "vite": "^8.2.1" + } +} diff --git a/apps/pwa/src/runtime.tsx b/apps/pwa/src/runtime.tsx new file mode 100644 index 0000000..a1947eb --- /dev/null +++ b/apps/pwa/src/runtime.tsx @@ -0,0 +1,189 @@ +import { useEffect } from "react"; +import { toast } from "sonner"; + +import { setDatabaseEntry } from "@tensamin/shared/indexedDb"; +import { isTauri } from "@tauri-apps/api/core"; + +import "./style.css"; + +const launchedFiles: File[] = []; +const fileListeners = new Set<(file: File) => void>(); + +function emitLaunchedFile(file: File) { + if (fileListeners.size === 0) launchedFiles.push(file); + else for (const listener of fileListeners) listener(file); +} + +export function subscribeTuFileLaunch(listener: (file: File) => void) { + fileListeners.add(listener); + for (const file of launchedFiles.splice(0)) listener(file); + return () => { + fileListeners.delete(listener); + }; +} + +function isInstalledPwa() { + return ( + window.matchMedia("(display-mode: standalone)").matches || + window.matchMedia("(display-mode: window-controls-overlay)").matches || + (navigator as Navigator & { standalone?: boolean }).standalone === true + ); +} + +function applicationServerKey(value: string) { + const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); + const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="); + return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0)); +} + +async function enablePush() { + if (!("Notification" in window)) + throw new Error("Notifications are not supported by this browser."); + const permission = await Notification.requestPermission(); + if (permission !== "granted") + throw new Error("Notification permission was not granted."); + + const publicKey = import.meta.env.VITE_WEB_PUSH_PUBLIC_KEY; + if ( + !publicKey || + !("serviceWorker" in navigator) || + !("PushManager" in window) + ) { + return; + } + const registration = await navigator.serviceWorker.ready; + const subscription = + (await registration.pushManager.getSubscription()) ?? + (await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: applicationServerKey(publicKey), + })); + await setDatabaseEntry("keys", "push-subscription", subscription.toJSON()); +} + +function InstalledPwaRuntime() { + useEffect(() => { + if ( + !("serviceWorker" in navigator) || + !["http:", "https:"].includes(window.location.protocol) + ) { + return; + } + let reloading = false; + const handleControllerChange = () => { + if (reloading) return; + reloading = true; + // A newly activated service worker must reload the document it controls. + // eslint-disable-next-line tensamin/no-window-location-reload + window.location.reload(); + }; + navigator.serviceWorker.addEventListener( + "controllerchange", + handleControllerChange, + ); + void navigator.serviceWorker + .register( + import.meta.env.DEV ? "/dev-sw.js?dev-sw" : "/serviceWorker.js", + { + type: "module", + }, + ) + .then((registration) => { + const watchWorker = (worker: ServiceWorker) => { + worker.addEventListener("statechange", () => { + if (worker.state !== "installed") return; + if (!navigator.serviceWorker.controller) { + toast.success("Tensamin is ready for offline startup"); + return; + } + toast("A Tensamin update is ready", { + duration: Infinity, + action: { + label: "Update", + onClick: () => worker.postMessage({ type: "SKIP_WAITING" }), + }, + }); + }); + }; + if (registration.installing) watchWorker(registration.installing); + registration.addEventListener("updatefound", () => { + if (registration.installing) watchWorker(registration.installing); + }); + }) + .catch((error: unknown) => { + console.error("Failed to register the Tensamin service worker", error); + }); + return () => { + navigator.serviceWorker.removeEventListener( + "controllerchange", + handleControllerChange, + ); + }; + }, []); + + useEffect(() => { + const launchQueue = ( + window as Window & { + launchQueue?: { + setConsumer: ( + consumer: (params: { + files?: Array<{ getFile: () => Promise }>; + }) => void, + ) => void; + }; + } + ).launchQueue; + launchQueue?.setConsumer((params) => { + for (const handle of params.files ?? []) { + void handle.getFile().then(emitLaunchedFile); + } + }); + }, []); + + useEffect(() => { + if ( + !("Notification" in window) || + Notification.permission !== "default" || + localStorage.getItem("pwa-push-hint") + ) { + return; + } + localStorage.setItem("pwa-push-hint", "shown"); + toast("Enable message notifications", { + duration: Infinity, + action: { + label: "Enable", + onClick: () => { + void enablePush() + .then(() => toast.success("Notifications enabled")) + .catch((error: unknown) => + toast.error( + error instanceof Error + ? error.message + : "Could not enable notifications", + ), + ); + }, + }, + }); + }, []); + + useEffect(() => { + if ( + "Notification" in window && + Notification.permission === "granted" && + import.meta.env.VITE_WEB_PUSH_PUBLIC_KEY + ) { + void enablePush().catch((error: unknown) => { + console.error("Failed to refresh the Web Push subscription", error); + }); + } + }, []); + + return null; +} + +export default function PwaRuntime() { + if (isTauri() || !isInstalledPwa()) return null; + return ; +} diff --git a/apps/pwa/src/serviceWorker.ts b/apps/pwa/src/serviceWorker.ts new file mode 100644 index 0000000..554ceeb --- /dev/null +++ b/apps/pwa/src/serviceWorker.ts @@ -0,0 +1,161 @@ +/// + +import { base64ToBytes } from "mtp"; +import { clientsClaim } from "workbox-core"; +import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching"; +import { NavigationRoute, registerRoute } from "workbox-routing"; +import { createHandlerBoundToURL } from "workbox-precaching"; +import { CacheFirst } from "workbox-strategies"; + +import { decryptChatText, unwrapChatSecret } from "@tensamin/crypto/chatSecret"; +import { loadSecureBrowserValue } from "@tensamin/storage/browserSecure"; + +declare let self: ServiceWorkerGlobalScope; + +type PushPayload = { + version: 1; + senderId: number; + sender: string; + avatar?: string; + message: { content: string }; + secret: { + chatId: string; + secretId: string; + version: number; + encryptedSecret: string; + kemCiphertext: string; + wrappingScheme: string; + }; +}; + +function isPushPayload(value: unknown): value is PushPayload { + if (!value || typeof value !== "object") return false; + const payload = value as Partial; + const message = payload.message as + Partial | undefined; + const secret = payload.secret as Partial | undefined; + const stringValues = [ + payload.sender, + message?.content, + secret?.chatId, + secret?.secretId, + secret?.encryptedSecret, + secret?.kemCiphertext, + secret?.wrappingScheme, + ]; + return ( + payload.version === 1 && + typeof payload.senderId === "number" && + Number.isSafeInteger(payload.senderId) && + payload.senderId > 0 && + typeof secret?.version === "number" && + stringValues.every((item) => typeof item === "string") + ); +} + +async function decryptPush(payload: PushPayload) { + const keyring = await loadSecureBrowserValue("mtp_keyring"); + if (!keyring) throw new Error("MTP credentials are unavailable."); + const chatSecret = await unwrapChatSecret({ + encryptedSecret: base64ToBytes(payload.secret.encryptedSecret), + kemCiphertext: base64ToBytes(payload.secret.kemCiphertext), + keyring, + chatId: payload.secret.chatId, + secretId: payload.secret.secretId, + version: payload.secret.version, + wrappingScheme: payload.secret.wrappingScheme, + }); + try { + return await decryptChatText(chatSecret, payload.message.content); + } finally { + chatSecret.fill(0); + } +} + +clientsClaim(); +cleanupOutdatedCaches(); +const precacheManifest = self.__WB_MANIFEST; +precacheAndRoute(precacheManifest); + +if ( + precacheManifest.some((entry) => + (typeof entry === "string" ? entry : entry.url).endsWith("index.html"), + ) +) { + registerRoute( + new NavigationRoute(createHandlerBoundToURL("index.html"), { + denylist: [/^\/api\//], + }), + ); +} +registerRoute( + ({ request, url }) => + url.origin === self.location.origin && + ["font", "image", "style"].includes(request.destination), + new CacheFirst({ cacheName: "tensamin-static-v1" }), +); + +self.addEventListener("push", (event) => { + event.waitUntil( + (async () => { + let payload: PushPayload | undefined; + try { + const value = event.data?.json() as unknown; + if (isPushPayload(value)) payload = value; + } catch { + // The generic notification below is safe for malformed payloads. + } + + let body = "Open Tensamin to view the encrypted message."; + if (payload) { + try { + body = await decryptPush(payload); + } catch { + // Do not leak credential or decryption failures in the notification. + } + } + + await self.registration.showNotification(payload?.sender ?? "Tensamin", { + body, + icon: payload?.avatar || "./icons/icon-192.png", + badge: "./icons/notification-badge.png", + tag: payload ? `message-${payload.senderId}` : "message", + data: { url: payload ? `/chat?id=${payload.senderId}` : "/" }, + }); + + const navigatorWithBadge = self.navigator as WorkerNavigator & { + setAppBadge?: (contents?: number) => Promise; + }; + await navigatorWithBadge.setAppBadge?.().catch(() => undefined); + })(), + ); +}); + +self.addEventListener("notificationclick", (event) => { + event.notification.close(); + event.waitUntil( + (async () => { + const target = new URL( + String( + (event.notification.data as { url?: string } | undefined)?.url ?? "/", + ), + self.location.origin, + ); + const windows = await self.clients.matchAll({ + type: "window", + includeUncontrolled: true, + }); + for (const client of windows) { + if ("navigate" in client) await client.navigate(target.href); + return client.focus(); + } + return self.clients.openWindow(target.href); + })(), + ); +}); + +self.addEventListener("message", (event) => { + if ((event.data as { type?: string } | undefined)?.type === "SKIP_WAITING") { + void self.skipWaiting(); + } +}); diff --git a/apps/pwa/src/style.css b/apps/pwa/src/style.css new file mode 100644 index 0000000..213d773 --- /dev/null +++ b/apps/pwa/src/style.css @@ -0,0 +1,18 @@ +@media (display-mode: standalone), (display-mode: fullscreen) { + [data-pwa-root] { + padding-top: env(safe-area-inset-top, 0px); + padding-right: env(safe-area-inset-right, 0px); + padding-left: env(safe-area-inset-left, 0px); + } +} + +@media (display-mode: window-controls-overlay) and (min-width: 768px) { + [data-pwa-navbar] { + min-height: env(titlebar-area-height, 3.375rem); + padding-left: max(1px, env(titlebar-area-x, 0px)); + padding-right: max( + 0px, + calc(100vw - env(titlebar-area-x, 0px) - env(titlebar-area-width, 100vw)) + ); + } +} diff --git a/apps/pwa/src/vite.ts b/apps/pwa/src/vite.ts new file mode 100644 index 0000000..962dce1 --- /dev/null +++ b/apps/pwa/src/vite.ts @@ -0,0 +1,218 @@ +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import type { Plugin } from "vite"; +import { VitePWA } from "vite-plugin-pwa"; + +const pwaDirectory = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const tauriIcons = resolve(pwaDirectory, "../tauri/src-tauri/icons"); +const androidResources = resolve( + pwaDirectory, + "../tauri/src-tauri/gen/android/app/src/main/res", +); + +function emitIcons(): Plugin { + const icons = [ + { + fileName: "icons/icon-180.png", + source: resolve(tauriIcons, "ios/AppIcon-60x60@3x.png"), + }, + { + fileName: "icons/icon-192.png", + source: resolve(androidResources, "mipmap-xxxhdpi/ic_launcher.png"), + }, + { + fileName: "icons/icon-96.png", + source: resolve(androidResources, "mipmap-xhdpi/ic_launcher.png"), + }, + { + fileName: "icons/icon-512.png", + source: resolve(tauriIcons, "icon.png"), + }, + { + fileName: "icons/icon-maskable-512.png", + source: resolve(tauriIcons, "icon.png"), + }, + { + fileName: "icons/icon-monochrome-432.png", + source: resolve( + androidResources, + "mipmap-xxxhdpi/ic_launcher_monochrome.png", + ), + }, + { + fileName: "icons/notification-badge.png", + source: resolve(androidResources, "drawable/ic_notification_small.png"), + }, + ]; + + return { + name: "tensamin-pwa-icons", + configureServer(server) { + server.middlewares.use((request, response, next) => { + const pathname = request.url + ? new URL(request.url, "http://localhost").pathname.slice(1) + : ""; + const icon = icons.find(({ fileName }) => fileName === pathname); + if (!icon) { + next(); + return; + } + response.statusCode = 200; + response.setHeader("Content-Type", "image/png"); + response.setHeader("Cache-Control", "no-cache"); + response.end(readFileSync(icon.source)); + }); + }, + generateBundle() { + for (const icon of icons) { + this.emitFile({ + type: "asset", + fileName: icon.fileName, + source: readFileSync(icon.source), + }); + } + }, + transformIndexHtml: { + order: "post", + handler() { + return [ + { + tag: "link", + attrs: { + rel: "apple-touch-icon", + sizes: "180x180", + href: "./icons/icon-180.png", + }, + injectTo: "head", + }, + { + tag: "meta", + attrs: { name: "apple-mobile-web-app-capable", content: "yes" }, + injectTo: "head", + }, + { + tag: "meta", + attrs: { + name: "apple-mobile-web-app-status-bar-style", + content: "black-translucent", + }, + injectTo: "head", + }, + { + tag: "meta", + attrs: { + name: "apple-mobile-web-app-title", + content: "Tensamin", + }, + injectTo: "head", + }, + { + tag: "meta", + attrs: { name: "theme-color", content: "#006a67" }, + injectTo: "head", + }, + ]; + }, + }, + }; +} + +export function tensaminPwa(): Plugin[] { + return [ + emitIcons(), + ...VitePWA({ + strategies: "injectManifest", + srcDir: resolve(pwaDirectory, "src"), + filename: "serviceWorker.ts", + injectRegister: null, + registerType: "prompt", + buildBase: "/", + manifestFilename: "manifest.json", + includeAssets: ["favicon.ico", "icons/*.png"], + manifest: { + id: "/", + name: "Tensamin", + short_name: "Tensamin", + description: "Private messaging and calls with Tensamin.", + start_url: "/", + scope: "/", + display: "standalone", + display_override: ["window-controls-overlay", "standalone"], + background_color: "#001f1e", + theme_color: "#006a67", + categories: ["social", "communication"], + orientation: "any", + launch_handler: { client_mode: "focus-existing" }, + icons: [ + { + src: "icons/icon-192.png", + sizes: "192x192", + type: "image/png", + purpose: "any", + }, + { + src: "icons/icon-512.png", + sizes: "512x512", + type: "image/png", + purpose: "any", + }, + { + src: "icons/icon-maskable-512.png", + sizes: "512x512", + type: "image/png", + purpose: "maskable", + }, + { + src: "icons/icon-monochrome-432.png", + sizes: "432x432", + type: "image/png", + purpose: "monochrome", + }, + ], + shortcuts: [ + { + name: "Chats", + short_name: "Chats", + url: "/", + icons: [ + { + src: "icons/icon-96.png", + sizes: "96x96", + type: "image/png", + }, + ], + }, + { + name: "Settings", + short_name: "Settings", + url: "/settings", + icons: [ + { + src: "icons/icon-96.png", + sizes: "96x96", + type: "image/png", + }, + ], + }, + ], + file_handlers: [ + { + action: "/login", + accept: { "application/x-tensamin-user": [".tu"] }, + }, + ], + }, + injectManifest: { + globPatterns: ["**/*.{js,css,html,ico,png,svg,woff2,wasm,mp3,wav}"], + globIgnores: ["assets/v2/**"], + maximumFileSizeToCacheInBytes: 15 * 1024 * 1024, + }, + devOptions: { + enabled: true, + type: "module", + }, + }), + ]; +} diff --git a/apps/pwa/todo.md b/apps/pwa/todo.md new file mode 100644 index 0000000..b5b2055 --- /dev/null +++ b/apps/pwa/todo.md @@ -0,0 +1,33 @@ +# Web Push Backend TODO + +The client can subscribe and decrypt version 1 push payloads, but reliable delivery requires backend support. + +- Generate and securely store a VAPID key pair. Expose only the public key to the web build as `VITE_WEB_PUSH_PUBLIC_KEY`. +- Add authenticated MTP requests for registering, replacing, and deleting a browser `PushSubscription` per user and installation. +- Persist the endpoint, `p256dh`, `auth`, expiration time, stable installation ID, and last-seen time. +- Remove subscriptions when a push service returns HTTP 404 or 410 and rate-limit registrations per user. +- Send pushes when an encrypted live message cannot be delivered to an active browser client. Define duplicate suppression for clients that receive both MTP and Web Push. +- Keep the JSON payload within push-provider limits and use this version 1 shape: + +```json +{ + "version": 1, + "senderId": 123, + "sender": "Display name", + "avatar": "https://optional.example/avatar", + "message": { "content": "base64 encrypted message content" }, + "secret": { + "chatId": "123:456", + "secretId": "chat:123:456:main", + "version": 1, + "encryptedSecret": "base64 wrapped chat secret", + "kemCiphertext": "base64 KEM ciphertext", + "wrappingScheme": "mtp-chat-secret-kem-chacha20poly1305-hkdf-sha256-v1" + } +} +``` + +- Ensure the wrapped secret is intended for the receiving user's MTP keyring. The server must never receive plaintext message content or plaintext chat secrets. +- Decide how edits, deletions, reactions, calls, read states, and per-chat notification cancellation map to push events. +- Add subscription rotation handling and unregister subscriptions when a user logs out or clears application data. +- Configure production HTTPS, SPA route fallback, `application/manifest+json` for `manifest.json`, and `Cache-Control: no-cache` for the service worker. diff --git a/apps/pwa/tsconfig.json b/apps/pwa/tsconfig.json new file mode 100644 index 0000000..1e7cfde --- /dev/null +++ b/apps/pwa/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client", "vite-plugin-pwa/client", "node"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/runtime.tsx", "src/vite.ts"] +} diff --git a/apps/pwa/tsconfig.worker.json b/apps/pwa/tsconfig.worker.json new file mode 100644 index 0000000..a478894 --- /dev/null +++ b/apps/pwa/tsconfig.worker.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "WebWorker"], + "types": ["vite-plugin-pwa/client"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/serviceWorker.ts"] +} diff --git a/apps/tauri/.gitignore b/apps/tauri/.gitignore index dc67136..8952e07 100644 --- a/apps/tauri/.gitignore +++ b/apps/tauri/.gitignore @@ -24,6 +24,3 @@ dist-ssr *.sw? .android - -/src-tauri/gen/android/keystore.properties -/src-tauri/gen/android/keystore.jks diff --git a/apps/tauri/monochrome_cropped.png b/apps/tauri/monochrome_cropped.png new file mode 100644 index 0000000..424ebd1 Binary files /dev/null and b/apps/tauri/monochrome_cropped.png differ diff --git a/apps/tauri/package.json b/apps/tauri/package.json index 27be94c..36937be 100644 --- a/apps/tauri/package.json +++ b/apps/tauri/package.json @@ -7,16 +7,13 @@ "./deeplinkHandler": { "types": "./src/deeplinkHandler.tsx", "default": "./src/deeplinkHandler.tsx" - }, - "./qrCodeScanner": { - "types": "./src/qrCodeScanner.tsx", - "default": "./src/qrCodeScanner.tsx" } }, "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", "dev:mobile": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#tauri --command pnpm run dev:mobile:raw; else pnpm run dev:mobile:raw; fi", "start-adb:mobile": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#tauri --command pnpm run start-adb:mobile:raw; else pnpm run start-adb:mobile:raw; fi", "build:mobile": "node render-version.ts && trap 'node render-version.ts --unrender' EXIT && if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#tauri --command pnpm run build:mobile:raw; else pnpm run build:mobile:raw; fi", @@ -25,18 +22,15 @@ "lint": "eslint src" }, "dependencies": { - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-barcode-scanner": "~2", - "@tauri-apps/plugin-deep-link": "~2", - "@tauri-apps/plugin-log": "~2", - "@tauri-apps/plugin-notification": "~2", - "@tensamin/shared": "workspace:*", "@methanium/ui": "*", - "react": "^19.2.0", - "react-dom": "^19.2.0" + "@tauri-apps/api": "^2.11.1", + "@tauri-apps/plugin-deep-link": "~2.4.9", + "@tensamin/shared": "workspace:*", + "react": "^19.2.8", + "react-dom": "^19.2.8" }, "devDependencies": { - "@tauri-apps/cli": "^2", - "@types/node": "^25.9.1" + "@tauri-apps/cli": "^2.11.4", + "@types/node": "^26.1.2" } } diff --git a/apps/tauri/scripts/delete-mobile.ts b/apps/tauri/scripts/delete-mobile.ts new file mode 100644 index 0000000..e41fa50 --- /dev/null +++ b/apps/tauri/scripts/delete-mobile.ts @@ -0,0 +1,53 @@ +import { spawnSync } from "node:child_process"; +import { createInterface } from "node:readline/promises"; + +const devicesResult = spawnSync("adb", ["devices", "-l"], { + encoding: "utf8", +}); + +if (devicesResult.status !== 0) { + process.stderr.write(devicesResult.stderr); + process.exit(devicesResult.status ?? 1); +} + +const devices = devicesResult.stdout + .split("\n") + .slice(1) + .map((line) => line.trim()) + .filter((line) => /\sdevice(?:\s|$)/.test(line)); + +if (devices.length === 0) { + console.error("No connected ADB devices found."); + process.exit(1); +} + +let selectedDevice = devices[0]; + +if (devices.length > 1) { + console.log("Select a device:"); + devices.forEach((device, index) => console.log(`${index + 1}) ${device}`)); + + const readline = createInterface({ + input: process.stdin, + output: process.stdout, + }); + const answer = await readline.question("Device: "); + readline.close(); + + const selectedIndex = Number(answer) - 1; + if (!Number.isInteger(selectedIndex) || !devices[selectedIndex]) { + console.error("Invalid device selection."); + process.exit(1); + } + + selectedDevice = devices[selectedIndex]; +} + +const serial = selectedDevice.split(/\s+/, 1)[0]; +const uninstallResult = spawnSync( + "adb", + ["-s", serial, "uninstall", "net.tensamin.client.dev"], + { stdio: "inherit" }, +); + +process.exit(uninstallResult.status ?? 1); diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index c872475..d404d61 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -9,21 +9,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] -name = "ahash" -version = "0.7.8" +name = "aead" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "getrandom 0.2.17", - "once_cell", - "version_check", + "crypto-common 0.1.7", + "generic-array", ] [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -62,24 +61,57 @@ dependencies = [ [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] -name = "arrayvec" -version = "0.7.8" +name = "asn1-rs" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.19", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "async-broadcast" @@ -174,7 +206,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -203,13 +235,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -247,6 +279,30 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "base64" version = "0.21.7" @@ -259,13 +315,25 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] [[package]] @@ -274,6 +342,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -282,25 +359,13 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] -[[package]] -name = "bitvec" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -310,6 +375,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -332,30 +406,6 @@ dependencies = [ "piper", ] -[[package]] -name = "borsh" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" -dependencies = [ - "borsh-derive", - "bytes", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" -dependencies = [ - "once_cell", - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "brotli" version = "8.0.4" @@ -392,45 +442,11 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "byte-unit" -version = "5.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a813de7f2bbedb7dce265b64f1cf5908ebe4d56281ece8d847e98113788b9b0" -dependencies = [ - "rust_decimal", - "schemars 1.2.1", - "serde", - "utf8-width", -] - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "byteorder" @@ -440,9 +456,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] @@ -453,7 +469,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cairo-sys-rs", "glib", "libc", @@ -474,9 +490,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.4" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" dependencies = [ "serde_core", ] @@ -501,26 +517,29 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "cargo_toml" -version = "0.22.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +checksum = "aa61aec073ec94791433ddf3df2323ff9d1711557c2a0eefb0f99cb4f8dca520" dependencies = [ + "semver", "serde", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", ] [[package]] name = "cc" -version = "1.2.65" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -559,9 +578,44 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] [[package]] name = "chrono" @@ -575,6 +629,32 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "combine" version = "4.6.7" @@ -594,6 +674,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -646,7 +732,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation", "core-graphics-types", "foreign-types", @@ -659,7 +745,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation", "libc", ] @@ -673,6 +759,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -684,18 +779,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crunchy" @@ -710,9 +805,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + [[package]] name = "cssparser" version = "0.36.0" @@ -733,7 +840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -746,12 +853,69 @@ dependencies = [ "dtor", ] +[[package]] +name = "ctor" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" + [[package]] name = "ctor-proc-macro" version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto 0.3.0", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "darling" version = "0.23.0" @@ -772,7 +936,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -783,20 +947,51 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] -name = "dbus" -version = "0.9.11" +name = "data-encoding" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ "libc", "libdbus-sys", "windows-sys 0.61.2", ] +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.8" @@ -824,7 +1019,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -833,8 +1028,20 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -864,7 +1071,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -872,13 +1079,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -901,7 +1108,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -979,6 +1186,31 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek 5.0.0", + "ed25519", + "serde", + "sha2 0.11.0", + "signature", + "subtle", + "zeroize", +] + [[package]] name = "embed-resource" version = "3.0.11" @@ -988,7 +1220,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "vswhom", "winreg", ] @@ -1023,7 +1255,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1060,16 +1292,15 @@ 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]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -1085,10 +1316,22 @@ dependencies = [ ] [[package]] -name = "fastrand" -version = "2.4.1" +name = "fastbloom" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" +dependencies = [ + "foldhash", + "libm", + "portable-atomic", + "siphasher", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fdeflate" @@ -1108,6 +1351,18 @@ dependencies = [ "log", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "field-offset" version = "0.3.6" @@ -1158,13 +1413,13 @@ dependencies = [ [[package]] name = "foreign-types-macros" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -1183,31 +1438,47 @@ dependencies = [ ] [[package]] -name = "funty" -version = "2.0.0" +name = "fs_extra" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", + "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1216,9 +1487,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-lite" @@ -1235,33 +1506,34 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1388,8 +1660,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1411,8 +1685,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] @@ -1453,7 +1730,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "futures-channel", "futures-core", "futures-executor", @@ -1481,7 +1758,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1496,9 +1773,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "gobject-sys" @@ -1560,7 +1837,82 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h3" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10872b55cfb02a821b69dc7cf8dc6a71d6af25eb9a79662bec4a9d016056b3be" +dependencies = [ + "bytes", + "fastrand", + "futures-util", + "http", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "h3-datagram" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2c9f77921668673721ae40f17c729fc48b9e38a663858097cea547484fdf0f" +dependencies = [ + "bytes", + "h3", + "pin-project-lite", +] + +[[package]] +name = "h3-quinn" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2e732c8d91a74731663ac8479ab505042fbf547b9a207213ab7fbcbfc4f8b4" +dependencies = [ + "bytes", + "futures", + "h3", + "h3-datagram", + "quinn", + "tokio", + "tokio-util", +] + +[[package]] +name = "h3-webtransport" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d91a50fd582a5d67b1f756fba3cd9c66367ff4f23e1017c882f664d63b350a7" +dependencies = [ + "bytes", + "futures-util", + "h3", + "h3-datagram", + "http", + "pin-project-lite", + "tokio", + "tracing", ] [[package]] @@ -1568,9 +1920,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] [[package]] name = "hashbrown" @@ -1608,6 +1957,24 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "html5ever" version = "0.38.0" @@ -1619,10 +1986,16 @@ dependencies = [ ] [[package]] -name = "http" -version = "1.4.2" +name = "httlib-huffman" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "1a9fcbcc408c5526c3ab80d534e5c86e7967c1fb7aa0a8c76abd1edc27deb877" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1630,9 +2003,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -1640,9 +2013,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -1658,18 +2031,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "hyper" -version = "1.10.1" +name = "httpdate" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "ctutils", + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1677,6 +2068,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1712,7 +2118,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -1876,10 +2282,19 @@ dependencies = [ ] [[package]] -name = "ipnet" -version = "2.12.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is-docker" @@ -1945,6 +2360,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.19", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -1970,7 +2415,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", ] [[package]] @@ -1990,12 +2445,24 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" dependencies = [ - "jsonptr", + "jsonptr 0.6.3", "serde", "serde_json", "thiserror 1.0.69", ] +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr 0.7.1", + "serde", + "serde_json", + "thiserror 2.0.19", +] + [[package]] name = "jsonptr" version = "0.6.3" @@ -2006,17 +2473,52 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd9697dc4a9a62e2da93389f34400b77a28f0287711263cabb203b3ccb9c0e4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + [[package]] name = "keyboard-types" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "serde", "unicode-segmentation", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libappindicator" version = "0.9.0" @@ -2043,9 +2545,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libdbus-sys" @@ -2067,10 +2569,16 @@ dependencies = [ ] [[package]] -name = "libredox" -version = "0.1.18" +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "libc", ] @@ -2101,9 +2609,12 @@ name = "log" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" -dependencies = [ - "value-bag", -] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "mac-notification-sys" @@ -2132,9 +2643,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -2151,6 +2662,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2163,15 +2680,216 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "ml-dsa" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" +dependencies = [ + "const-oid", + "crypto-common 0.2.2", + "ctutils", + "hybrid-array", + "module-lattice", + "pkcs8", + "shake", + "signature", +] + +[[package]] +name = "mlkem-rs" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b0965b8b081668ff0398dc5e9dc3f2ebb9e833393f4ab5b9f725ddce11acef8" +dependencies = [ + "rand_core 0.6.4", + "serde", + "sha3", + "subtle", + "zeroize", +] + +[[package]] +name = "mlkem-tls" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77b205d031298adf904d88efd6a57862d8650a4ab754aade19a9b5e87040bf4e" +dependencies = [ + "mlkem-rs", + "rand_core 0.6.4", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + +[[package]] +name = "mtp" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +dependencies = [ + "mtp-client", + "mtp-codec", + "mtp-common", + "mtp-crypto", + "mtp-host", + "mtp-transport", + "mtp-type-map", + "mtp-webserver", +] + +[[package]] +name = "mtp-client" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +dependencies = [ + "mtp-codec", + "mtp-common", + "mtp-crypto", + "mtp-transport", + "rand 0.10.2", + "tokio", +] + +[[package]] +name = "mtp-codec" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +dependencies = [ + "base64 0.23.1", + "byteorder", + "mtp-common", + "mtp-crypto", + "mtp-type-map", + "rand 0.10.2", + "thiserror 2.0.19", +] + +[[package]] +name = "mtp-common" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +dependencies = [ + "quinn", + "rustls", + "thiserror 2.0.19", + "wtransport", +] + +[[package]] +name = "mtp-crypto" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +dependencies = [ + "base64 0.22.1", + "chacha20poly1305", + "ed25519-dalek", + "getrandom 0.4.3", + "hkdf", + "ml-dsa", + "mlkem-tls", + "rand 0.10.2", + "rand_core 0.6.4", + "rustls", + "serde", + "sha2 0.11.0", + "thiserror 1.0.69", + "tokio", + "zeroize", +] + +[[package]] +name = "mtp-host" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +dependencies = [ + "mtp-codec", + "mtp-common", + "mtp-crypto", + "mtp-transport", + "rand 0.10.2", + "thiserror 2.0.19", + "tokio", + "tracing", + "wtransport", +] + +[[package]] +name = "mtp-transport" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +dependencies = [ + "async-trait", + "mtp-codec", + "mtp-common", + "mtp-crypto", + "rand 0.10.2", + "rcgen", + "rustls", + "rustls-native-certs", + "sha2 0.11.0", + "tokio", + "tracing", + "wtransport", + "zeroize", +] + +[[package]] +name = "mtp-type-map" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +dependencies = [ + "serde", + "serde_yaml", +] + +[[package]] +name = "mtp-webserver" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +dependencies = [ + "async-trait", + "bytes", + "h3", + "h3-quinn", + "h3-webtransport", + "http", + "http-body-util", + "hyper", + "hyper-util", + "mtp-codec", + "mtp-common", + "mtp-crypto", + "mtp-host", + "mtp-transport", + "quinn", + "rustls", + "thiserror 2.0.19", + "tokio", + "tokio-rustls", + "tokio-stream", + "tracing", +] + [[package]] name = "muda" version = "0.19.3" @@ -2189,7 +2907,7 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "windows-sys 0.61.2", ] @@ -2199,7 +2917,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "jni-sys 0.3.1", "log", "ndk-sys", @@ -2223,6 +2941,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify-rust" version = "4.18.0" @@ -2237,12 +2965,31 @@ dependencies = [ "zbus", ] +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2271,7 +3018,7 @@ dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2299,7 +3046,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "objc2", "objc2-core-foundation", @@ -2312,7 +3059,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-foundation", ] @@ -2333,7 +3080,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2", ] @@ -2344,7 +3091,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -2377,7 +3124,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -2404,7 +3151,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -2417,7 +3164,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", ] @@ -2438,7 +3185,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -2450,7 +3197,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", ] @@ -2461,7 +3208,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "objc2", "objc2-cloud-kit", @@ -2492,7 +3239,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "objc2", "objc2-app-kit", @@ -2502,6 +3249,21 @@ dependencies = [ "objc2-security", ] +[[package]] +name = "octets" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "866cb5af6f3aa3c1b44c3c2d79d22165fbb1b102e1b3fb499864bfe34736ec4b" + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2509,16 +3271,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "open" -version = "5.3.6" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "open" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cfef937e9c486488c7e3d949ae31c0f1d06bdacd75b99c086cb35356e30408" dependencies = [ "dunce", "is-wsl", "libc", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2599,6 +3373,25 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2646,7 +3439,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2675,6 +3468,16 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -2683,9 +3486,9 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", @@ -2713,7 +3516,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", @@ -2734,6 +3537,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + [[package]] name = "potential_utf" version = "0.1.5" @@ -2790,7 +3610,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] @@ -2819,47 +3639,87 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "quick-xml" -version = "0.39.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] [[package]] -name = "quote" -version = "1.0.46" +name = "quinn" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "futures-io", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.19", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "fastbloom", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.19", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -2876,41 +3736,25 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" -version = "0.8.6" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "rand" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "chacha20 0.10.1", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -2941,19 +3785,49 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "aws-lc-rs", + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -2964,34 +3838,34 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] name = "regex" -version = "1.12.4" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3001,9 +3875,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -3016,15 +3890,6 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - [[package]] name = "reqwest" version = "0.13.4" @@ -3039,15 +3904,21 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3060,32 +3931,17 @@ dependencies = [ ] [[package]] -name = "rkyv" -version = "0.7.46" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", ] [[package]] @@ -3098,28 +3954,11 @@ dependencies = [ "ordered-multimap", ] -[[package]] -name = "rust_decimal" -version = "1.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.6", - "rkyv", - "serde", - "serde_json", - "wasm-bindgen", -] - [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -3130,24 +3969,116 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", ] [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3158,6 +4089,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3187,13 +4127,13 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "ref-cast", - "schemars_derive 1.2.1", + "schemars_derive 1.2.2", "serde", "serde_json", "url", @@ -3208,20 +4148,20 @@ checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" dependencies = [ "proc-macro2", "quote", - "serde_derive_internals", - "syn 2.0.118", + "serde_derive_internals 0.29.1", + "syn 2.0.119", ] [[package]] name = "schemars_derive" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" dependencies = [ "proc-macro2", "quote", - "serde_derive_internals", - "syn 2.0.118", + "serde_derive_internals 0.30.0", + "syn 3.0.3", ] [[package]] @@ -3231,10 +4171,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "seahash" -version = "4.1.0" +name = "security-framework" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] name = "selectors" @@ -3242,7 +4199,7 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cssparser", "derive_more", "log", @@ -3267,9 +4224,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -3289,22 +4246,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -3315,14 +4272,25 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap 2.14.0", "itoa", @@ -3334,13 +4302,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -3374,7 +4342,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.14.0", "schemars 0.9.0", - "schemars 1.2.1", + "schemars 1.2.2", "serde_core", "serde_json", "serde_with_macros", @@ -3390,7 +4358,20 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", ] [[package]] @@ -3412,7 +4393,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3431,8 +4412,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.6", +] + +[[package]] +name = "shake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.1", + "sponge-cursor", ] [[package]] @@ -3452,10 +4465,30 @@ dependencies = [ ] [[package]] -name = "simd-adler32" -version = "0.3.9" +name = "signature" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] [[package]] name = "simdutf8" @@ -3483,9 +4516,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3539,6 +4572,22 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3575,6 +4624,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3591,6 +4646,16 @@ name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -3599,9 +4664,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -3625,7 +4690,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3647,7 +4712,7 @@ version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "core-foundation", "core-graphics", @@ -3659,7 +4724,7 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", "ndk", @@ -3676,28 +4741,22 @@ dependencies = [ "unicode-segmentation", "url", "windows", - "windows-core 0.61.2", + "windows-core", "windows-version", "x11-dl", ] [[package]] name = "tao-macros" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "target-lexicon" version = "0.12.16" @@ -3706,8 +4765,8 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.3" -source = "git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef#20f3f2515c65ca1e115991c3ef3625e2c29c1bbb" +version = "2.11.5" +source = "git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41#4af26a3f7f8b692d62cca549bbacd93f5ce90b41" dependencies = [ "anyhow", "bytes", @@ -3720,7 +4779,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -3743,8 +4802,8 @@ dependencies = [ "tauri-macros", "tauri-runtime", "tauri-runtime-wry", - "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef)", - "thiserror 2.0.18", + "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41)", + "thiserror 2.0.19", "tokio", "tray-icon", "url", @@ -3757,19 +4816,19 @@ dependencies = [ [[package]] name = "tauri-build" version = "2.6.3" -source = "git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef#20f3f2515c65ca1e115991c3ef3625e2c29c1bbb" +source = "git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41#4af26a3f7f8b692d62cca549bbacd93f5ce90b41" dependencies = [ "anyhow", "cargo_toml", "dirs", "glob", "heck 0.5.0", - "json-patch", - "schemars 1.2.1", + "json-patch 4.2.0", + "schemars 1.2.2", "semver", "serde", "serde_json", - "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef)", + "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41)", "tauri-winres", "walkdir", ] @@ -3777,12 +4836,12 @@ dependencies = [ [[package]] name = "tauri-codegen" version = "2.6.3" -source = "git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef#20f3f2515c65ca1e115991c3ef3625e2c29c1bbb" +source = "git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41#4af26a3f7f8b692d62cca549bbacd93f5ce90b41" dependencies = [ "base64 0.22.1", "brotli", "ico", - "json-patch", + "json-patch 4.2.0", "plist", "png 0.17.16", "proc-macro2", @@ -3790,10 +4849,10 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", - "syn 2.0.118", - "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef)", - "thiserror 2.0.18", + "sha2 0.10.9", + "syn 2.0.119", + "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41)", + "thiserror 2.0.19", "time", "url", "uuid", @@ -3803,14 +4862,14 @@ dependencies = [ [[package]] name = "tauri-macros" version = "2.6.3" -source = "git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef#20f3f2515c65ca1e115991c3ef3625e2c29c1bbb" +source = "git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41#4af26a3f7f8b692d62cca549bbacd93f5ce90b41" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "tauri-codegen", - "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef)", + "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41)", ] [[package]] @@ -3829,32 +4888,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "tauri-plugin-app-events" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e625f35abcfc422c7c4af8b348d2069cdf387a39c12429fdce570137d02f069" -dependencies = [ - "serde", - "tauri", - "tauri-plugin", - "thiserror 1.0.69", -] - -[[package]] -name = "tauri-plugin-barcode-scanner" -version = "2.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b68f0e3782b61a6e16a67380be40226e2b7233f3e36dadf6e5d03f07d2e7b3" -dependencies = [ - "log", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", -] - [[package]] name = "tauri-plugin-deep-link" version = "2.4.9" @@ -3869,21 +4902,20 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils 2.9.3 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "url", "windows-registry", - "windows-result 0.3.4", + "windows-result", ] [[package]] name = "tauri-plugin-log" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" +checksum = "6792296e6f389268016c77db21ebae1fc0568f2fccf88b1ec7e2ea71330afb4c" dependencies = [ "android_logger", - "byte-unit", "fern", "log", "objc2", @@ -3894,7 +4926,7 @@ dependencies = [ "swift-rs", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", ] @@ -3906,13 +4938,13 @@ checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" dependencies = [ "log", "notify-rust", - "rand 0.9.4", + "rand 0.9.5", "serde", "serde_json", "serde_repr", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "url", ] @@ -3933,7 +4965,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", "windows", "zbus", @@ -3942,32 +4974,32 @@ dependencies = [ [[package]] name = "tauri-runtime" version = "2.11.3" -source = "git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef#20f3f2515c65ca1e115991c3ef3625e2c29c1bbb" +source = "git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41#4af26a3f7f8b692d62cca549bbacd93f5ce90b41" dependencies = [ "cookie", "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "raw-window-handle", "serde", "serde_json", - "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef)", - "thiserror 2.0.18", + "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41)", + "thiserror 2.0.19", "url", "windows", ] [[package]] name = "tauri-runtime-wry" -version = "2.11.3" -source = "git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef#20f3f2515c65ca1e115991c3ef3625e2c29c1bbb" +version = "2.11.4" +source = "git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41#4af26a3f7f8b692d62cca549bbacd93f5ce90b41" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -3978,7 +5010,7 @@ dependencies = [ "softbuffer", "tao", "tauri-runtime", - "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef)", + "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41)", "url", "webkit2gtk", "webview2-com", @@ -3994,13 +5026,13 @@ checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "anyhow", "cargo_metadata", - "ctor", + "ctor 0.8.0", "dom_query", "dunce", "glob", "http", "infer", - "json-patch", + "json-patch 3.0.1", "log", "memchr", "phf", @@ -4015,28 +5047,28 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", "url", - "urlpattern", + "urlpattern 0.3.0", "uuid", ] [[package]] name = "tauri-utils" version = "2.9.3" -source = "git+https://github.com/tauri-apps/tauri?branch=feat%2Fcef#20f3f2515c65ca1e115991c3ef3625e2c29c1bbb" +source = "git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41#4af26a3f7f8b692d62cca549bbacd93f5ce90b41" dependencies = [ "anyhow", "brotli", "cargo_metadata", - "ctor", + "ctor 1.0.12", "dom_query", "dunce", "glob", "http", "infer", - "json-patch", + "json-patch 4.2.0", "log", "memchr", "phf", @@ -4044,17 +5076,17 @@ dependencies = [ "proc-macro2", "quote", "regex", - "schemars 1.2.1", + "schemars 1.2.2", "semver", "serde", "serde-untagged", "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", "url", - "urlpattern", + "urlpattern 0.6.0", "uuid", "walkdir", ] @@ -4067,7 +5099,7 @@ checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", ] [[package]] @@ -4076,7 +5108,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", "windows", "windows-version", ] @@ -4088,36 +5120,39 @@ 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]] name = "tendril" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" dependencies = [ "new_debug_unreachable", - "utf-8", ] [[package]] name = "tensamin" version = "0.0.0" dependencies = [ + "base64 0.22.1", + "jni 0.22.4", + "mtp", + "reqwest", "serde", "serde_json", "tauri", "tauri-build", - "tauri-plugin-app-events", - "tauri-plugin-barcode-scanner", "tauri-plugin-deep-link", "tauri-plugin-log", "tauri-plugin-notification", "tauri-plugin-opener", + "tokio", + "webpki-root-certs", ] [[package]] @@ -4131,11 +5166,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -4146,25 +5181,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] name = "time" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "libc", @@ -4184,9 +5219,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -4213,9 +5248,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -4228,27 +5263,63 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] [[package]] -name = "tokio-util" -version = "0.7.18" +name = "tokio-macros" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -4267,24 +5338,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -4292,7 +5348,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -4304,15 +5360,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -4348,30 +5395,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -4394,7 +5441,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -4424,6 +5471,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -4437,7 +5485,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4451,9 +5499,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" dependencies = [ "crossbeam-channel", "dirs", @@ -4467,7 +5515,7 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "windows-sys 0.61.2", ] @@ -4553,6 +5601,34 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4579,16 +5655,16 @@ dependencies = [ ] [[package]] -name = "utf-8" -version = "0.7.6" +name = "urlpattern" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" +dependencies = [ + "icu_properties", + "regex", + "serde", + "url", +] [[package]] name = "utf8_iter" @@ -4598,9 +5674,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.4" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -4608,12 +5684,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" - [[package]] name = "version-compare" version = "0.2.1" @@ -4722,7 +5792,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -4758,6 +5828,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.5" @@ -4814,6 +5894,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -4823,7 +5912,7 @@ dependencies = [ "webview2-com-macros", "webview2-com-sys", "windows", - "windows-core 0.61.2", + "windows-core", "windows-implement", "windows-interface", ] @@ -4836,7 +5925,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4845,9 +5934,9 @@ version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", "windows", - "windows-core 0.61.2", + "windows-core", ] [[package]] @@ -4872,7 +5961,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]] @@ -4903,7 +5992,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", @@ -4915,7 +6004,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]] @@ -4927,21 +6016,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]] @@ -4950,7 +6026,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", ] @@ -4963,7 +6039,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4974,7 +6050,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4995,7 +6071,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", ] @@ -5006,8 +6082,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]] @@ -5019,15 +6095,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" @@ -5037,15 +6104,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" @@ -5055,6 +6113,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5223,15 +6290,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -5276,7 +6337,7 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -5288,27 +6349,54 @@ dependencies = [ "once_cell", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.9", "soup3", "tao-macros", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", "webkit2gtk", "webkit2gtk-sys", "webview2-com", "windows", - "windows-core 0.61.2", + "windows-core", "windows-version", "x11-dl", ] [[package]] -name = "wyz" -version = "0.5.1" +name = "wtransport" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +checksum = "ea4aacf790813ee1956751491800537f4e04af7557b7b370501ccbfbc85963e4" dependencies = [ - "tap", + "bytes", + "pem", + "quinn", + "rcgen", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "sha2 0.11.0", + "socket2", + "thiserror 2.0.19", + "time", + "tokio", + "tracing", + "url", + "wtransport-proto", + "x509-parser", +] + +[[package]] +name = "wtransport-proto" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5867c629e4252f7439d82315923daaf27f4fa442410d51b78ab93ef4c432a11" +dependencies = [ + "httlib-huffman", + "octets", + "thiserror 2.0.19", + "url", ] [[package]] @@ -5332,6 +6420,47 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.19", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec 0.9.1", + "time", +] + [[package]] name = "yoke" version = "0.8.3" @@ -5351,15 +6480,15 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] [[package]] name = "zbus" -version = "5.16.0" +version = "5.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" dependencies = [ "async-broadcast", "async-executor", @@ -5384,7 +6513,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 1.0.3", + "winnow 1.0.4", "zbus_macros", "zbus_names", "zvariant", @@ -5392,14 +6521,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.16.0" +version = "5.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "zbus_names", "zvariant", "zvariant_utils", @@ -5407,33 +6536,33 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.2" +version = "4.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" dependencies = [ "serde", - "winnow 1.0.3", + "winnow 1.0.4", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5453,10 +6582,30 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zerotrie" version = "0.2.4" @@ -5487,51 +6636,51 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zvariant" -version = "5.12.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" dependencies = [ "endi", "enumflags2", "serde", - "winnow 1.0.3", + "winnow 1.0.4", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.12.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.118", - "winnow 1.0.3", + "syn 2.0.119", + "winnow 1.0.4", ] diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 1d3bdf5..2a68e95 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -15,12 +15,17 @@ name = "mobile_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] -tauri-build = { git = "https://github.com/tauri-apps/tauri", branch = "feat/cef", 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"] } +tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } +mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "a5c8d4f0c898c78351e9d54124886c86e789a22a", features = ["client", "crypto"] } +webpki-root-certs = "1" tauri-plugin-deep-link = "2" tauri-plugin-notification = "2" tauri-plugin-log = "2" @@ -30,10 +35,14 @@ version = "2" features = [] default-features = true -[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -tauri-plugin-barcode-scanner = "2" -tauri-plugin-app-events = "0.2" +[target.'cfg(not(target_os = "android"))'.dependencies.tauri] +version = "2" +features = [] +default-features = true + +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.22" [patch.crates-io.tauri] git = "https://github.com/tauri-apps/tauri" -branch = "feat/cef" +rev = "4af26a3f7f8b692d62cca549bbacd93f5ce90b41" diff --git a/apps/tauri/src-tauri/capabilities/default.json b/apps/tauri/src-tauri/capabilities/default.json index 73d510a..83f7620 100644 --- a/apps/tauri/src-tauri/capabilities/default.json +++ b/apps/tauri/src-tauri/capabilities/default.json @@ -6,10 +6,7 @@ "main" ], "permissions": [ - "core:default", "opener:default", - "core:window:default", - "core:event:default", "deep-link:default", "notification:default", "log:default" diff --git a/apps/tauri/src-tauri/capabilities/mobile.json b/apps/tauri/src-tauri/capabilities/mobile.json index 342dd9e..91f1d4e 100644 --- a/apps/tauri/src-tauri/capabilities/mobile.json +++ b/apps/tauri/src-tauri/capabilities/mobile.json @@ -1,18 +1,10 @@ { "identifier": "mobile-capability", - "platforms": [ - "android", - "iOS" - ], - "windows": [ - "main" - ], + "platforms": ["android", "iOS"], + "windows": ["main"], "permissions": [ + "core:event:default", "deep-link:default", - "app-events:default", - "barcode-scanner:default", - "barcode-scanner:allow-scan", - "barcode-scanner:allow-cancel", "notification:default", "log:default" ] diff --git a/apps/tauri/src-tauri/gen/android/app/build.gradle.kts b/apps/tauri/src-tauri/gen/android/app/build.gradle.kts index 3775319..66d6289 100644 --- a/apps/tauri/src-tauri/gen/android/app/build.gradle.kts +++ b/apps/tauri/src-tauri/gen/android/app/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.util.Properties import java.io.FileInputStream @@ -61,26 +62,29 @@ android { ) } } - kotlinOptions { - jvmTarget = "1.8" - } buildFeatures { buildConfig = true } } +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 + } +} + rust { rootDirRel = "../../../" } dependencies { - implementation("androidx.webkit:webkit:1.14.0") + implementation("androidx.webkit:webkit:1.16.0") implementation("androidx.appcompat:appcompat:1.7.1") - implementation("androidx.activity:activity-ktx:1.10.1") - implementation("com.google.android.material:material:1.12.0") + implementation("androidx.activity:activity-ktx:1.13.0") + implementation("com.google.android.material:material:1.14.0") testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.4") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") + androidTestImplementation("androidx.test.ext:junit:1.3.0") + androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") } -apply(from = "tauri.build.gradle.kts") \ No newline at end of file +apply(from = "tauri.build.gradle.kts") diff --git a/apps/tauri/src-tauri/gen/android/app/proguard-rules.pro b/apps/tauri/src-tauri/gen/android/app/proguard-rules.pro index 481bb43..f1b4245 100644 --- a/apps/tauri/src-tauri/gen/android/app/proguard-rules.pro +++ b/apps/tauri/src-tauri/gen/android/app/proguard-rules.pro @@ -18,4 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/apps/tauri/src-tauri/gen/android/app/src/main/AndroidManifest.xml index 697b23b..7d05c07 100644 --- a/apps/tauri/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/apps/tauri/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,16 @@ + + + + + + + + + + @@ -39,6 +49,30 @@ + + + + + + + + + + + + + val includeAudio = pendingScreenAudio + pendingScreenAudio = null + + if (includeAudio == null) return@registerForActivityResult + val data = result.data + if (result.resultCode != Activity.RESULT_OK || data == null) { + MobileMediaEvents.emitError("Screen capture permission was denied") + return@registerForActivityResult + } + + MediaProjectionService.start(this, result.resultCode, data, includeAudio) + } + + private val cameraPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions(), + ) { + emitCameraPermission() + } + + private val screenAudioPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { + launchScreenCaptureIntent() + } + + override fun onWebViewCreate(webView: WebView) { + webView.settings.apply { + setSupportZoom(false) + builtInZoomControls = false + displayZoomControls = false + } + + var blockingMultiTouch = false + webView.setOnTouchListener { _, event -> + val shouldBlock = blockingMultiTouch || event.pointerCount > 1 + + when (event.actionMasked) { + MotionEvent.ACTION_POINTER_DOWN -> { + // Cancel the one-finger gesture before consuming the rest of the pinch. + MotionEvent.obtain(event).let { cancelEvent -> + cancelEvent.action = MotionEvent.ACTION_CANCEL + webView.onTouchEvent(cancelEvent) + cancelEvent.recycle() + } + blockingMultiTouch = true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> blockingMultiTouch = false + } + + shouldBlock + } + + NativeAccessibilityBridge.attach(webView) + mediaWebView = webView + MobileMediaEvents.attach(webView) + webView.addJavascriptInterface(MobileMediaJavascriptInterface(), "tensaminMobileMedia") + } 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) + } + NativeAccessibilityBridge.nativeAttach(applicationContext) installKeyboardResizeWorkaround() } + override fun onResume() { + super.onResume() + NativeMtpBridge.nativeSetUiState(true) + } + + override fun onPause() { + NativeMtpBridge.nativeSetUiState(false) + super.onPause() + } + override fun onDestroy() { attachLayoutListener?.let { listener -> contentRoot?.viewTreeObserver?.removeOnGlobalLayoutListener(listener) @@ -30,9 +116,87 @@ class MainActivity : TauriActivity() { attachLayoutListener = null contentRoot = null contentChild = null + mediaWebView?.let { + NativeAccessibilityBridge.detach(it) + it.removeJavascriptInterface("tensaminMobileMedia") + } + mediaWebView = null + MobileMediaEvents.detach() super.onDestroy() } + private fun startScreenShare(includeAudio: Boolean) { + runOnUiThread { + if (pendingScreenAudio != null) { + MobileMediaEvents.emitError("Screen capture permission is already pending") + return@runOnUiThread + } + + pendingScreenAudio = includeAudio + if ( + includeAudio && + ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != + PackageManager.PERMISSION_GRANTED + ) { + screenAudioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) + } else { + launchScreenCaptureIntent() + } + } + } + + private fun launchScreenCaptureIntent() { + val manager = getSystemService(MediaProjectionManager::class.java) + screenCaptureLauncher.launch(manager.createScreenCaptureIntent()) + } + + private fun stopScreenShare() { + runOnUiThread { + pendingScreenAudio = null + MediaProjectionService.stop(this) + } + } + + private fun requestCameraPermission() { + runOnUiThread { + cameraPermissionLauncher.launch( + arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO), + ) + } + } + + private fun emitCameraPermission() { + val detail = JSONObject() + .put( + "camera", + ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == + PackageManager.PERMISSION_GRANTED, + ) + .put( + "microphone", + ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == + PackageManager.PERMISSION_GRANTED, + ) + MobileMediaEvents.emit("tensamin-mobile-camera-permission", detail) + } + + private inner class MobileMediaJavascriptInterface { + @JavascriptInterface + fun startScreenShare(includeAudio: Boolean) { + this@MainActivity.startScreenShare(includeAudio) + } + + @JavascriptInterface + fun stopScreenShare() { + this@MainActivity.stopScreenShare() + } + + @JavascriptInterface + fun requestCameraPermission() { + this@MainActivity.requestCameraPermission() + } + } + private fun installKeyboardResizeWorkaround() { val content = window.decorView.findViewById(android.R.id.content) contentRoot = content @@ -68,16 +232,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 diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MediaProjectionService.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MediaProjectionService.kt new file mode 100644 index 0000000..3e961df --- /dev/null +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MediaProjectionService.kt @@ -0,0 +1,345 @@ +package net.tensamin.client + +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo +import android.graphics.Bitmap +import android.graphics.PixelFormat +import android.hardware.display.DisplayManager +import android.hardware.display.VirtualDisplay +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioPlaybackCaptureConfiguration +import android.media.AudioRecord +import android.media.projection.MediaProjection +import android.media.projection.MediaProjectionManager +import android.media.ImageReader +import android.os.Build +import android.os.Handler +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 +import org.json.JSONObject + +class MediaProjectionService : Service() { + private var projection: MediaProjection? = null + private var virtualDisplay: VirtualDisplay? = null + private var imageReader: ImageReader? = null + private var captureThread: HandlerThread? = null + private var audioRecord: AudioRecord? = null + private var audioThread: Thread? = null + private val captureActive = AtomicBoolean(false) + private var lastFrameAt = 0L + + private val projectionCallback = object : MediaProjection.Callback() { + override fun onStop() { + stopCapture(stopProjection = false, emitStopped = true) + stopSelf() + } + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent?.action == ACTION_STOP) { + stopCapture(stopProjection = true, emitStopped = true) + stopSelf() + return START_NOT_STICKY + } + + val permissionData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent?.getParcelableExtra(EXTRA_PERMISSION_DATA, Intent::class.java) + } else { + @Suppress("DEPRECATION") + intent?.getParcelableExtra(EXTRA_PERMISSION_DATA) + } + val resultCode = intent?.getIntExtra(EXTRA_RESULT_CODE, Int.MIN_VALUE) ?: Int.MIN_VALUE + if (permissionData == null || resultCode == Int.MIN_VALUE) { + MobileMediaEvents.emitError("Screen capture permission data is missing") + stopSelf() + return START_NOT_STICKY + } + + val includeAudio = intent?.getBooleanExtra(EXTRA_INCLUDE_AUDIO, false) ?: false + try { + startForegroundNotification() + startCapture(resultCode, permissionData, includeAudio) + } catch (error: Throwable) { + stopCapture(stopProjection = true, emitStopped = false) + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + MobileMediaEvents.emitError(error.message ?: "Unable to start screen capture") + } + return START_NOT_STICKY + } + + override fun onDestroy() { + stopCapture(stopProjection = true, emitStopped = true) + super.onDestroy() + } + + private fun startForegroundNotification() { + val notificationManager = getSystemService(NotificationManager::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + notificationManager.createNotificationChannel( + NotificationChannel( + NOTIFICATION_CHANNEL_ID, + "Screen sharing", + NotificationManager.IMPORTANCE_LOW, + ), + ) + } + + val stopIntent = Intent(this, MediaProjectionService::class.java).setAction(ACTION_STOP) + val stopPendingIntent = PendingIntent.getService( + this, + 0, + stopIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + 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(NotificationCompat.CATEGORY_SERVICE) + .addAction(android.R.drawable.ic_media_pause, "Stop", stopPendingIntent) + .build() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION, + ) + } else { + startForeground(NOTIFICATION_ID, notification) + } + } + + @Suppress("DEPRECATION") + private fun startCapture(resultCode: Int, permissionData: Intent, includeAudio: Boolean) { + stopCapture(stopProjection = true, emitStopped = false) + + val manager = getSystemService(MediaProjectionManager::class.java) + val newProjection = manager.getMediaProjection(resultCode, permissionData) + ?: error("Android did not provide a screen capture session") + projection = newProjection + + val thread = HandlerThread("tensamin-screen-capture").also { it.start() } + captureThread = thread + val handler = Handler(thread.looper) + newProjection.registerCallback(projectionCallback, handler) + + val metrics = DisplayMetrics() + getSystemService(android.view.WindowManager::class.java).defaultDisplay.getRealMetrics(metrics) + val displayWidth = metrics.widthPixels + val displayHeight = metrics.heightPixels + val width = minOf(displayWidth, MAX_FRAME_WIDTH) + val height = (displayHeight.toLong() * width / displayWidth).toInt() + val density = metrics.densityDpi + val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2) + imageReader = reader + reader.setOnImageAvailableListener({ source -> captureFrame(source, width, height) }, handler) + + virtualDisplay = newProjection.createVirtualDisplay( + "Tensamin screen sharing", + width, + height, + density, + DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, + reader.surface, + null, + handler, + ) + captureActive.set(true) + + val audioStarted = includeAudio && startAudioCapture(newProjection) + MobileMediaEvents.emit( + "tensamin-mobile-screen-started", + JSONObject().put("includeAudio", audioStarted), + ) + } + + private fun captureFrame(source: ImageReader, width: Int, height: Int) { + val image = source.acquireLatestImage() ?: return + try { + val now = System.currentTimeMillis() + if (!captureActive.get() || now - lastFrameAt < FRAME_INTERVAL_MS) return + lastFrameAt = now + + val plane = image.planes[0] + val paddedWidth = plane.rowStride / plane.pixelStride + val paddedBitmap = Bitmap.createBitmap(paddedWidth, height, Bitmap.Config.ARGB_8888) + paddedBitmap.copyPixelsFromBuffer(plane.buffer) + val croppedBitmap = if (paddedWidth == width) { + paddedBitmap + } else { + Bitmap.createBitmap(paddedBitmap, 0, 0, width, height).also { paddedBitmap.recycle() } + } + val outputBitmap = croppedBitmap + + val bytes = ByteArrayOutputStream() + outputBitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, bytes) + val outputWidth = outputBitmap.width + val outputHeight = outputBitmap.height + outputBitmap.recycle() + MobileMediaEvents.emit( + "tensamin-mobile-screen-frame", + JSONObject() + .put("data", Base64.encodeToString(bytes.toByteArray(), Base64.NO_WRAP)) + .put("mimeType", "image/jpeg") + .put("width", outputWidth) + .put("height", outputHeight), + ) + } catch (error: Throwable) { + if (captureActive.get()) { + MobileMediaEvents.emitError(error.message ?: "Unable to read a screen frame") + } + } finally { + image.close() + } + } + + private fun startAudioCapture(activeProjection: MediaProjection): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + MobileMediaEvents.emitError("System audio capture requires Android 10 or newer") + return false + } + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != + PackageManager.PERMISSION_GRANTED + ) { + MobileMediaEvents.emitError("Microphone permission is required for system audio capture") + return false + } + + return try { + val format = AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(AUDIO_SAMPLE_RATE) + .setChannelMask(AudioFormat.CHANNEL_IN_MONO) + .build() + val configuration = AudioPlaybackCaptureConfiguration.Builder(activeProjection) + .addMatchingUsage(AudioAttributes.USAGE_MEDIA) + .addMatchingUsage(AudioAttributes.USAGE_GAME) + .build() + val minimumBuffer = AudioRecord.getMinBufferSize( + AUDIO_SAMPLE_RATE, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT, + ) + check(minimumBuffer > 0) { "Android could not allocate a system audio buffer" } + val bufferSize = maxOf(minimumBuffer * 2, AUDIO_BATCH_BYTES) + val record = AudioRecord.Builder() + .setAudioFormat(format) + .setAudioPlaybackCaptureConfig(configuration) + .setBufferSizeInBytes(bufferSize) + .build() + check(record.state == AudioRecord.STATE_INITIALIZED) { + "Android could not initialize system audio capture" + } + audioRecord = record + record.startRecording() + audioThread = Thread({ readAudio(record) }, "tensamin-audio-capture").also { it.start() } + true + } catch (error: Throwable) { + audioRecord?.release() + audioRecord = null + MobileMediaEvents.emitError(error.message ?: "Unable to capture system audio") + false + } + } + + private fun readAudio(record: AudioRecord) { + val buffer = ByteArray(AUDIO_BATCH_BYTES) + while (captureActive.get() && !Thread.currentThread().isInterrupted) { + val read = record.read(buffer, 0, buffer.size, AudioRecord.READ_BLOCKING) + if (read > 0 && captureActive.get()) { + MobileMediaEvents.emit( + "tensamin-mobile-screen-audio", + JSONObject() + .put("data", Base64.encodeToString(buffer, 0, read, Base64.NO_WRAP)) + .put("sampleRate", AUDIO_SAMPLE_RATE) + .put("channelCount", 1) + .put("encoding", "pcm16le"), + ) + } else if (read < 0 && captureActive.get()) { + MobileMediaEvents.emitError("System audio capture stopped with code $read") + return + } + } + } + + @Synchronized + private fun stopCapture(stopProjection: Boolean, emitStopped: Boolean) { + val wasActive = captureActive.getAndSet(false) + + val record = audioRecord + audioRecord = null + try { + record?.stop() + } catch (_: IllegalStateException) { + } + record?.release() + audioThread?.interrupt() + audioThread = null + + imageReader?.setOnImageAvailableListener(null, null) + virtualDisplay?.release() + virtualDisplay = null + imageReader?.close() + imageReader = null + + val oldProjection = projection + projection = null + oldProjection?.unregisterCallback(projectionCallback) + if (stopProjection) oldProjection?.stop() + + captureThread?.quitSafely() + captureThread = null + lastFrameAt = 0L + + if (wasActive && emitStopped) { + MobileMediaEvents.emit("tensamin-mobile-screen-stopped") + } + } + + companion object { + private const val ACTION_STOP = "net.tensamin.client.STOP_SCREEN_SHARE" + private const val EXTRA_RESULT_CODE = "resultCode" + private const val EXTRA_PERMISSION_DATA = "permissionData" + private const val EXTRA_INCLUDE_AUDIO = "includeAudio" + private const val NOTIFICATION_CHANNEL_ID = "screen-sharing" + private const val NOTIFICATION_ID = 7314 + private const val FRAME_INTERVAL_MS = 75L + private const val MAX_FRAME_WIDTH = 1280 + private const val JPEG_QUALITY = 72 + private const val AUDIO_SAMPLE_RATE = 48_000 + private const val AUDIO_BATCH_BYTES = 9_600 + + fun start(context: Context, resultCode: Int, data: Intent, includeAudio: Boolean) { + val intent = Intent(context, MediaProjectionService::class.java) + .putExtra(EXTRA_RESULT_CODE, resultCode) + .putExtra(EXTRA_PERMISSION_DATA, data) + .putExtra(EXTRA_INCLUDE_AUDIO, includeAudio) + ContextCompat.startForegroundService(context, intent) + } + + fun stop(context: Context) { + context.startService( + Intent(context, MediaProjectionService::class.java).setAction(ACTION_STOP), + ) + } + } +} diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MobileMediaEvents.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MobileMediaEvents.kt new file mode 100644 index 0000000..c8c377c --- /dev/null +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MobileMediaEvents.kt @@ -0,0 +1,29 @@ +package net.tensamin.client + +import android.webkit.WebView +import java.lang.ref.WeakReference +import org.json.JSONObject + +object MobileMediaEvents { + private var webView = WeakReference(null) + + fun attach(value: WebView) { + webView = WeakReference(value) + } + + fun detach() { + webView.clear() + } + + fun emitError(message: String) { + emit("tensamin-mobile-screen-error", JSONObject().put("message", message)) + } + + fun emit(name: String, detail: JSONObject = JSONObject()) { + val view = webView.get() ?: return + val script = "window.dispatchEvent(new CustomEvent(" + + JSONObject.quote(name) + + ", { detail: " + detail.toString() + " }));" + view.post { view.evaluateJavascript(script, null) } + } +} diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpBootReceiver.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpBootReceiver.kt new file mode 100644 index 0000000..b96ae35 --- /dev/null +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpBootReceiver.kt @@ -0,0 +1,17 @@ +package net.tensamin.client + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class MtpBootReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if ( + intent.action == Intent.ACTION_BOOT_COMPLETED && + MtpSecureStore.isEnabled(context) && + MtpSecureStore.hasConfig(context) + ) { + NativeMtpBridge.startService(context) + } + } +} diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpForegroundService.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpForegroundService.kt new file mode 100644 index 0000000..56cca97 --- /dev/null +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpForegroundService.kt @@ -0,0 +1,167 @@ +package net.tensamin.client + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.net.ConnectivityManager +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() { + private var started = false + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) = refreshNotification() + override fun onLost(network: Network) = refreshNotification() + override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) = + refreshNotification() + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onCreate() { + super.onCreate() + getSystemService(ConnectivityManager::class.java) + .registerDefaultNetworkCallback(networkCallback) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent?.action == ACTION_STOP) { + MtpSecureStore.setEnabled(this, false) + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + return START_NOT_STICKY + } + + if (started) return START_STICKY + + createChannel(this) + connectionStatus = "Connecting" + val notification = buildNotification(this, displayedStatus(this)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground( + NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE, + ) + } else { + startForeground(NOTIFICATION_ID, notification) + } + val config = MtpSecureStore.loadConfig(this) + if (config == null || !MtpSecureStore.isEnabled(this)) { + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + return START_NOT_STICKY + } + + try { + NativeMtpBridge.nativeAttach(applicationContext) + NativeMtpBridge.nativeStart(config) + started = true + NativeMtpBridge.log(2, "Started MTP foreground service") + } catch (error: Throwable) { + NativeMtpBridge.log(0, "Failed to start MTP foreground service", error) + updateNotification(this, "Connection failed") + } + return START_STICKY + } + + 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() { + getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback) + if (!MtpSecureStore.isEnabled(this)) NativeMtpBridge.nativeStop() + super.onDestroy() + } + + private fun refreshNotification() { + updateNotification(this, connectionStatus) + } + + companion object { + 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) { + if (!MtpSecureStore.isEnabled(context)) return + connectionStatus = status + createChannel(context) + context.getSystemService(NotificationManager::class.java) + .notify(NOTIFICATION_ID, buildNotification(context, displayedStatus(context))) + } + + private fun displayedStatus(context: Context): String { + val connectivity = context.getSystemService(ConnectivityManager::class.java) + val network = connectivity.activeNetwork ?: return "No network" + val capabilities = connectivity.getNetworkCapabilities(network) ?: return "No network" + return if ( + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + ) connectionStatus else "No network" + } + + private fun createChannel(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + context.getSystemService(NotificationManager::class.java).createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + "Background connection", + NotificationManager.IMPORTANCE_LOW, + ).apply { description = "Keeps Tensamin connected for incoming messages" }, + ) + } + + private fun buildNotification(context: Context, status: String): Notification { + val openIntent = PendingIntent.getActivity( + context, + 0, + Intent(context, MainActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + val stopIntent = PendingIntent.getService( + context, + 1, + Intent(context, MtpForegroundService::class.java).setAction(ACTION_STOP), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + return NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_notify_sync) + .setContentTitle("Tensamin") + .setContentText(status) + .setContentIntent(openIntent) + .setOngoing(true) + .setCategory(Notification.CATEGORY_SERVICE) + .setPriority(NotificationCompat.PRIORITY_LOW) + .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopIntent) + .build() + } + } +} diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpSecureStore.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpSecureStore.kt new file mode 100644 index 0000000..bb03688 --- /dev/null +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpSecureStore.kt @@ -0,0 +1,74 @@ +package net.tensamin.client + +import android.content.Context +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec + +object MtpSecureStore { + private const val KEY_ALIAS = "tensamin-mtp-config" + private const val PREFS = "tensamin-mtp" + private const val CONFIG = "config" + private const val ENABLED = "enabled" + + fun saveConfig(context: Context, config: String) { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey()) + val encrypted = cipher.doFinal(config.toByteArray(Charsets.UTF_8)) + val payload = Base64.encodeToString(cipher.iv + encrypted, Base64.NO_WRAP) + preferences(context).edit().putString(CONFIG, payload).apply() + } + + fun loadConfig(context: Context): String? { + val payload = preferences(context).getString(CONFIG, null) ?: return null + return runCatching { + val bytes = Base64.decode(payload, Base64.NO_WRAP) + require(bytes.size > 12) + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init( + Cipher.DECRYPT_MODE, + getOrCreateKey(), + GCMParameterSpec(128, bytes.copyOfRange(0, 12)), + ) + String(cipher.doFinal(bytes.copyOfRange(12, bytes.size)), Charsets.UTF_8) + }.getOrNull() + } + + fun hasConfig(context: Context): Boolean = loadConfig(context) != null + + fun setEnabled(context: Context, enabled: Boolean) { + preferences(context).edit().putBoolean(ENABLED, enabled).apply() + } + + fun isEnabled(context: Context): Boolean = + preferences(context).getBoolean(ENABLED, false) + + private fun preferences(context: Context) = + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + + private fun getOrCreateKey(): SecretKey { + val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + (keyStore.getKey(KEY_ALIAS, null) as? SecretKey)?.let { return it } + + val generator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + "AndroidKeyStore", + ) + generator.init( + KeyGenParameterSpec.Builder( + KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT, + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + .build(), + ) + return generator.generateKey() + } +} diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/NativeAccessibilityBridge.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/NativeAccessibilityBridge.kt new file mode 100644 index 0000000..e04365a --- /dev/null +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/NativeAccessibilityBridge.kt @@ -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(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) } + } + } +} diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/NativeMtpBridge.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/NativeMtpBridge.kt new file mode 100644 index 0000000..ba9422d --- /dev/null +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/NativeMtpBridge.kt @@ -0,0 +1,161 @@ +package net.tensamin.client + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import android.provider.Settings +import android.util.Log +import androidx.annotation.Keep +import androidx.core.app.NotificationCompat +import androidx.core.app.Person +import androidx.core.content.LocusIdCompat +import androidx.core.content.ContextCompat +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat + +@Keep +object NativeMtpBridge { + private const val MESSAGE_CHANNEL = "tensamin-messages" + + init { + System.loadLibrary("mobile_lib") + } + + @JvmStatic external fun nativeAttach(context: Context) + @JvmStatic external fun nativeStart(config: String) + @JvmStatic external fun nativeStop() + @JvmStatic external fun nativeSetUiState(visible: Boolean) + @JvmStatic external fun nativeLog(level: Int, message: String, details: String) + + fun log(level: Int, message: String, error: Throwable? = null) { + val details = error?.stackTraceToString().orEmpty() + Log.println(if (level == 0) Log.ERROR else Log.INFO, "TensaminAndroid", "$message $details") + nativeLog(level, message, details) + } + + fun storeConfig(context: Context, config: String) { + try { + MtpSecureStore.saveConfig(context, config) + if (MtpSecureStore.isEnabled(context)) startService(context) + log(2, "Stored native MTP credentials") + } catch (error: Throwable) { + log(0, "Failed to store native MTP credentials", error) + throw error + } + } + + fun hasConfig(context: Context): Boolean = MtpSecureStore.hasConfig(context) + + fun setServiceEnabled(context: Context, enabled: Boolean) { + MtpSecureStore.setEnabled(context, enabled) + if (enabled && MtpSecureStore.hasConfig(context)) startService(context) else stopService(context) + } + + fun isIgnoringBatteryOptimizations(context: Context): Boolean = + context.getSystemService(PowerManager::class.java) + .isIgnoringBatteryOptimizations(context.packageName) + + fun requestBatteryExemption(context: Context) { + val intent = Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:${context.packageName}"), + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + fun startService(context: Context) { + ContextCompat.startForegroundService( + context, + Intent(context, MtpForegroundService::class.java), + ) + } + + fun stopService(context: Context) { + if (!context.stopService(Intent(context, MtpForegroundService::class.java))) nativeStop() + } + + fun updateServiceStatus(context: Context, status: String) { + MtpForegroundService.updateNotification(context, status) + } + + fun postMessageNotification( + context: Context, + senderId: Long, + sender: String, + body: String, + avatar: ByteArray, + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.getSystemService(NotificationManager::class.java).createNotificationChannel( + NotificationChannel( + MESSAGE_CHANNEL, + "Messages", + NotificationManager.IMPORTANCE_HIGH, + ).apply { description = "Incoming Tensamin messages" }, + ) + } + val openIntent = Intent( + Intent.ACTION_VIEW, + Uri.parse("tensamin://chat?id=$senderId"), + context, + MainActivity::class.java, + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + val pendingIntent = PendingIntent.getActivity( + context, + senderId.hashCode(), + openIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + val avatarBitmap = avatar.takeIf { it.isNotEmpty() }?.let { + BitmapFactory.decodeByteArray(it, 0, it.size) + } + val avatarIcon = avatarBitmap?.let(IconCompat::createWithAdaptiveBitmap) + val person = Person.Builder() + .setName(sender) + .setKey(senderId.toString()) + .setIcon(avatarIcon) + .build() + val shortcutId = "chat-$senderId" + val shortcut = ShortcutInfoCompat.Builder(context, shortcutId) + .setShortLabel(sender) + .setLongLived(true) + .setPerson(person) + .setIntent(openIntent) + .apply { if (avatarIcon != null) setIcon(avatarIcon) } + .build() + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + + val style = NotificationCompat.MessagingStyle( + Person.Builder().setName("You").build(), + ).addMessage(body, System.currentTimeMillis(), person) + val notification = NotificationCompat.Builder(context, MESSAGE_CHANNEL) + .setSmallIcon(R.drawable.ic_notification_small) + .setContentTitle(sender) + .setContentText(body) + .setStyle(style) + .setShortcutId(shortcutId) + .setLocusId(LocusIdCompat(shortcutId)) + .setLargeIcon(avatarBitmap) + .setCategory(Notification.CATEGORY_MESSAGE) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .build() + context.getSystemService(NotificationManager::class.java) + .notify(senderId.hashCode(), notification) + } + + fun cancelMessageNotification(context: Context, senderId: Long) { + context.getSystemService(NotificationManager::class.java) + .cancel(senderId.hashCode()) + } + +} diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/res/drawable/ic_notification_small.png b/apps/tauri/src-tauri/gen/android/app/src/main/res/drawable/ic_notification_small.png new file mode 100644 index 0000000..bec055d Binary files /dev/null and b/apps/tauri/src-tauri/gen/android/app/src/main/res/drawable/ic_notification_small.png differ diff --git a/apps/tauri/src-tauri/gen/android/build.gradle.kts b/apps/tauri/src-tauri/gen/android/build.gradle.kts index dddb93c..1488b27 100644 --- a/apps/tauri/src-tauri/gen/android/build.gradle.kts +++ b/apps/tauri/src-tauri/gen/android/build.gradle.kts @@ -1,4 +1,4 @@ -import com.android.build.gradle.LibraryExtension +import com.android.build.api.dsl.LibraryExtension buildscript { repositories { @@ -7,7 +7,7 @@ buildscript { } dependencies { classpath("com.android.tools.build:gradle:8.11.0") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20") } } diff --git a/apps/tauri/src-tauri/gen/android/buildSrc/build.gradle.kts b/apps/tauri/src-tauri/gen/android/buildSrc/build.gradle.kts index 5c55bba..09776c6 100644 --- a/apps/tauri/src-tauri/gen/android/buildSrc/build.gradle.kts +++ b/apps/tauri/src-tauri/gen/android/buildSrc/build.gradle.kts @@ -20,4 +20,3 @@ dependencies { compileOnly(gradleApi()) implementation("com.android.tools.build:gradle:8.11.0") } - diff --git a/apps/tauri/src-tauri/gen/android/buildSrc/src/main/java/net/tensamin/client/kotlin/BuildTask.kt b/apps/tauri/src-tauri/gen/android/buildSrc/src/main/java/net/tensamin/client/kotlin/BuildTask.kt index a7e39eb..e2be2d1 100644 --- a/apps/tauri/src-tauri/gen/android/buildSrc/src/main/java/net/tensamin/client/kotlin/BuildTask.kt +++ b/apps/tauri/src-tauri/gen/android/buildSrc/src/main/java/net/tensamin/client/kotlin/BuildTask.kt @@ -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() } -} \ No newline at end of file +} diff --git a/apps/tauri/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties b/apps/tauri/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties index c5f9a53..8e0bb4b 100644 --- a/apps/tauri/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties +++ b/apps/tauri/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue May 10 19:22:52 CST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/apps/tauri/src-tauri/icons/icon.icns b/apps/tauri/src-tauri/icons/icon.icns index 7f3b591..a7f0a1e 100644 Binary files a/apps/tauri/src-tauri/icons/icon.icns and b/apps/tauri/src-tauri/icons/icon.icns differ diff --git a/apps/tauri/src-tauri/src/accessibility_backend.rs b/apps/tauri/src-tauri/src/accessibility_backend.rs new file mode 100644 index 0000000..692b42e --- /dev/null +++ b/apps/tauri/src-tauri/src/accessibility_backend.rs @@ -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 { + 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 { + 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>, + bridge: Global>, + } + + static HOST: OnceLock = 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(call: impl FnOnce(&mut Env, &Host) -> Result) -> Result { + 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 { + 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, +}; diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index 2a51174..6636682 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -1,21 +1,37 @@ +mod accessibility_backend; +mod mtp_backend; + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let builder = tauri::Builder::default() - .plugin(tauri_plugin_log::Builder::new().level(tauri_plugin_log::log::LevelFilter::Info).build()) + .plugin( + tauri_plugin_log::Builder::new() + .level(tauri_plugin_log::log::LevelFilter::Info) + .build(), + ) .plugin(tauri_plugin_notification::init()); let builder = builder .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_opener::init()); - #[cfg(any(target_os = "ios", target_os = "android"))] - let builder = builder.plugin(tauri_plugin_barcode_scanner::init()); - - #[cfg(any(target_os = "ios", target_os = "android"))] - let builder = builder.plugin(tauri_plugin_app_events::init()); - - if let Err(error) = builder + 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, + mtp_backend::mtp_has_credentials, + mtp_backend::mtp_load_keyring, + mtp_backend::mtp_set_enabled, + mtp_backend::mtp_set_ui_visible, + mtp_backend::mtp_post_message_notification, + mtp_backend::mtp_is_ignoring_battery_optimizations, + mtp_backend::mtp_request_battery_exemption, + ]) .setup(|_app| { + mtp_backend::manager().attach_app(_app.handle().clone()); #[cfg(any(target_os = "linux", windows))] { use tauri_plugin_deep_link::DeepLinkExt; @@ -30,9 +46,18 @@ pub fn run() { } Ok(()) }) - .run(tauri::generate_context!()) - { - eprintln!("error while running tauri application: {error}"); - panic!("error while running tauri application: {error}"); - } + .build(tauri::generate_context!()) + .expect("error while building tauri application"); + + app.run(|_app, event| { + #[cfg(target_os = "android")] + if let tauri::RunEvent::ExitRequested { + api, code: None, .. + } = event + { + if mtp_backend::manager().is_enabled() { + api.prevent_exit(); + } + } + }); } diff --git a/apps/tauri/src-tauri/src/main.rs b/apps/tauri/src-tauri/src/main.rs index e78a26a..272583a 100644 --- a/apps/tauri/src-tauri/src/main.rs +++ b/apps/tauri/src-tauri/src/main.rs @@ -1,9 +1,6 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use lib::log; - fn main() { mobile_lib::run(); - log("Test test 123") } diff --git a/apps/tauri/src-tauri/src/mtp_backend.rs b/apps/tauri/src-tauri/src/mtp_backend.rs new file mode 100644 index 0000000..fa28597 --- /dev/null +++ b/apps/tauri/src-tauri/src/mtp_backend.rs @@ -0,0 +1,1563 @@ +use std::sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + Arc, Mutex, OnceLock, RwLock, +}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use base64::{ + engine::general_purpose::{STANDARD, STANDARD_NO_PAD}, + Engine as _, +}; +use mtp::client::{ClientConfig, MTPClient, MTPConnection, Policy, SendMode}; +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue, TypeMap}; +use mtp::crypto::{ + derive_encryption_key, AeadDecrypt, ChaCha20Poly1305, HybridKem, Keyring, PublicKeyBundle, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use tauri::{AppHandle, Emitter}; +use tokio::sync::mpsc; + +const EVENT_NAME: &str = "mtp://event"; +const DISCONNECTED: u8 = 0; +const CONNECTING: u8 = 1; +const CONNECTED: u8 = 2; +const CHAT_SECRET_SALT: &[u8] = b"tensamin-chat-secret-v1"; +const CHAT_MESSAGE_SALT: &[u8] = b"tensamin-chat-message-v1"; +const CHAT_SECRET_SCHEME: &str = "mtp-chat-secret-kem-chacha20poly1305-hkdf-sha256-v1"; +const INITIAL_SYNC_TIMEOUT: Duration = Duration::from_secs(30); +const MAX_BUFFERED_INITIAL_FRAMES: usize = 1_000; +const NOTIFICATION_QUEUE_CAPACITY: usize = 32; +#[cfg(target_os = "android")] +const ROOT_YE_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\n\ +MIIB2TCCAWCgAwIBAgIRAKQCa6LvbHwg1AR+XmWmk4AwCgYIKoZIzj0EAwMwLjEL\n\ +MAkGA1UEBhMCVVMxDTALBgNVBAoTBElTUkcxEDAOBgNVBAMTB1Jvb3QgWUUwHhcN\n\ +MjUwOTAzMDAwMDAwWhcNNDUwOTAyMjM1OTU5WjAuMQswCQYDVQQGEwJVUzENMAsG\n\ +A1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZRTB2MBAGByqGSM49AgEGBSuBBAAi\n\ +A2IABDwS/6vhrcVqcbBo+wgdI3fwn9x7DNJJOY/lTOti0vkwuRN87RhEhTH17E7X\n\ +yFjWsPYhIPt/wzOqxTd2b+4ZJNy9ID04YywF9U5zasDVyGSNErVNtz8uSGh5izW8\n\ +7j77GaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\n\ +BBYEFKPIJlqOoUzQNWP8myPIOq5W809WMAoGCCqGSM49BAMDA2cAMGQCMHhMr8N9\n\ +LdL1VQKs9BdV81r76eXRB6mtjuNjzk6/lBsPNToWLTDzGYgtQKO1jl63uAIwGV7m\n\ +onyF377c+MM1oqVNs17sgu7F9YKZwgLmVbeOMDbKAXHtKMDLbiGllCcs8f47\n\ +-----END CERTIFICATE-----\n"; +#[cfg(target_os = "android")] +const YE1_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\n\ +MIICizCCAhGgAwIBAgIQXd1w3TH4AchcGGp6BLgK/jAKBggqhkjOPQQDAzAuMQsw\n\ +CQYDVQQGEwJVUzENMAsGA1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZRTAeFw0y\n\ +NTA5MDMwMDAwMDBaFw0yODA5MDIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYwFAYD\n\ +VQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQDEwNZRTEwdjAQBgcqhkjOPQIBBgUr\n\ +gQQAIgNiAAQHZVB1/mimla2hfSurylScjPMZaOJXLz/NnAc2sylm8WDyhU9Ccp+z\n\ +ASQi5vSwGGJjSGklkD9fdPR8GpyDIOIjCEfrnbt/v+ZSEPLLEGbaM6EccDbN7p9x\n\ +teIm2Avf+ryjge4wgeswDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF\n\ +BwMBMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLsgykcL/tflnPmPCSqj\n\ +jDdFsbzYMB8GA1UdIwQYMBaAFKPIJlqOoUzQNWP8myPIOq5W809WMDIGCCsGAQUF\n\ +BwEBBCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3llLmkubGVuY3Iub3JnLzATBgNV\n\ +HSAEDDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veWUuYy5s\n\ +ZW5jci5vcmcvMAoGCCqGSM49BAMDA2gAMGUCMQDgjUEahFT/h3DRakqiPZpLvPgf\n\ +Zwkt6K2EOMmh1nvEzl83eMLYcod4GCl3b0J1Nn0CMBNYmEQJb4CEG5WoOe7aRn/L\n\ +VKu6saHmHEynI7ysIPd8zQsK1HdmhlHKlw9Z5GpGvA==\n\ +-----END CERTIFICATE-----\n"; + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MtpConfig { + pub user_id: u64, + pub keyring: String, + pub omega_url: String, + pub forced_omikron_url: Option, + pub forced_omikron_public_key: Option, +} + +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MtpSnapshot { + pub generation: u64, + pub ready_state: u8, + pub identified: bool, + pub state: Option, + pub error: Option, +} + +#[derive(Clone, Serialize)] +#[serde(tag = "kind", rename_all = "camelCase")] +enum MtpEvent { + State { + snapshot: MtpSnapshot, + }, + Message { + generation: u64, + message: Value, + }, + Log { + level: u8, + message: String, + details: Option, + }, +} + +pub struct MtpManager { + runtime: tokio::runtime::Runtime, + config: RwLock>, + connection: RwLock>>, + snapshot: RwLock, + generation: AtomicU64, + enabled: AtomicBool, + ui_visible: AtomicBool, + app: RwLock>, + start_lock: Mutex<()>, +} + +struct RequestIdAllocator { + next: AtomicU64, +} + +impl RequestIdAllocator { + fn new() -> Self { + Self { + next: AtomicU64::new(1), + } + } + + fn next(&self) -> Result { + let value = self.next.fetch_add(1, Ordering::Relaxed); + u32::try_from(value) + .map_err(|_| "MTP request ID space exhausted for this connection".to_string()) + } +} + +struct ManagedConnection { + mtp: Arc, + request_ids: RequestIdAllocator, +} + +impl ManagedConnection { + async fn next_request_id(&self) -> Result { + let id = self.request_ids.next(); + if id.is_err() { + self.mtp.sender.close().await; + } + id + } +} + +struct PreparedConnection { + connection: MTPConnection, + request_ids: RequestIdAllocator, + initial_state: Value, + buffered_frames: Vec, +} + +static MANAGER: OnceLock = OnceLock::new(); + +pub fn manager() -> &'static MtpManager { + MANAGER.get_or_init(|| MtpManager { + runtime: tokio::runtime::Builder::new_multi_thread() + .enable_all() + .thread_name("tensamin-mtp") + .build() + .expect("failed to create MTP runtime"), + config: RwLock::new(None), + connection: RwLock::new(None), + snapshot: RwLock::new(MtpSnapshot { + ready_state: DISCONNECTED, + ..MtpSnapshot::default() + }), + generation: AtomicU64::new(0), + enabled: AtomicBool::new(false), + ui_visible: AtomicBool::new(false), + app: RwLock::new(None), + start_lock: Mutex::new(()), + }) +} + +impl MtpManager { + pub fn attach_app(&self, app: AppHandle) { + *self.app.write().expect("app lock poisoned") = Some(app); + } + + pub fn configure_and_start(&'static self, config: MtpConfig) { + let _guard = self.start_lock.lock().expect("start lock poisoned"); + if self.enabled.load(Ordering::SeqCst) + && self.config.read().expect("config lock poisoned").as_ref() == Some(&config) + { + return; + } + *self.config.write().expect("config lock poisoned") = Some(config); + self.enabled.store(true, Ordering::SeqCst); + let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1; + if let Some(connection) = self + .connection + .write() + .expect("connection lock poisoned") + .take() + { + self.runtime + .spawn(async move { connection.mtp.sender.close().await }); + } + self.set_snapshot(MtpSnapshot { + generation, + ready_state: CONNECTING, + identified: false, + state: None, + error: None, + }); + self.runtime.spawn(supervise(generation)); + } + + pub fn stop(&self) { + self.enabled.store(false, Ordering::SeqCst); + self.generation.fetch_add(1, Ordering::SeqCst); + if let Some(connection) = self + .connection + .write() + .expect("connection lock poisoned") + .take() + { + self.runtime + .spawn(async move { connection.mtp.sender.close().await }); + } + self.set_snapshot(MtpSnapshot { + generation: self.generation.load(Ordering::SeqCst), + ready_state: DISCONNECTED, + identified: false, + state: None, + error: None, + }); + } + + pub fn set_ui_visible(&self, visible: bool) { + self.ui_visible.store(visible, Ordering::SeqCst); + } + + pub fn is_enabled(&self) -> bool { + self.enabled.load(Ordering::SeqCst) + } + + pub fn snapshot(&self) -> MtpSnapshot { + self.snapshot + .read() + .expect("snapshot lock poisoned") + .clone() + } + + fn set_snapshot(&self, snapshot: MtpSnapshot) { + if let Some(error) = snapshot.error.as_ref() { + eprintln!("android: MTP connection failed: {error}"); + } + *self.snapshot.write().expect("snapshot lock poisoned") = snapshot.clone(); + self.emit(MtpEvent::State { snapshot }); + } + + fn emit(&self, event: MtpEvent) { + if let Some(app) = self.app.read().expect("app lock poisoned").as_ref() { + let _ = app.emit(EVENT_NAME, event); + } + } + + fn log(&self, level: u8, message: impl Into, details: Option) { + let message = message.into(); + eprintln!( + "android: {message}{}", + details + .as_ref() + .map(|value| format!(": {value}")) + .unwrap_or_default() + ); + self.emit(MtpEvent::Log { + level, + message, + details, + }); + } + + fn is_current(&self, generation: u64) -> bool { + self.enabled.load(Ordering::SeqCst) && self.generation.load(Ordering::SeqCst) == generation + } + + async fn request(&self, type_name: &str, data: Value) -> Result { + let connection = self + .connection + .read() + .map_err(|_| "MTP connection lock is unavailable")? + .clone() + .ok_or_else(|| "MTP is not connected".to_string())?; + let request = json_to_frame(type_name, data, connection.next_request_id().await?)?; + let response = connection + .mtp + .request(&request, None) + .await + .map_err(|error| error.to_string())?; + frame_to_json(&response) + } +} + +async fn supervise(generation: u64) { + let manager = manager(); + let mut delay = Duration::from_secs(1); + while manager.is_current(generation) { + let config = manager.config.read().expect("config lock poisoned").clone(); + let Some(config) = config else { return }; + manager.log(2, "Starting native MTP connection", None); + android_status("Connecting"); + match connect(&config).await { + Ok(prepared) => { + delay = Duration::from_secs(1); + let connection = Arc::new(ManagedConnection { + mtp: Arc::new(prepared.connection), + request_ids: prepared.request_ids, + }); + let stale = { + let _guard = manager.start_lock.lock().expect("start lock poisoned"); + let mut current = manager + .connection + .write() + .expect("connection lock poisoned"); + if !manager.is_current(generation) { + true + } else { + *current = Some(connection.clone()); + drop(current); + manager.set_snapshot(MtpSnapshot { + generation, + ready_state: CONNECTED, + identified: true, + state: Some(prepared.initial_state), + error: None, + }); + false + } + }; + if stale { + connection.mtp.sender.close().await; + break; + } + android_status("Connected"); + manager.log(2, "Native MTP connection established", None); + + let (notification_tx, mut notification_rx) = + mpsc::channel(NOTIFICATION_QUEUE_CAPACITY); + let notification_connection = connection.clone(); + let notification_config = config.clone(); + let notification_worker = tokio::spawn(async move { + while let Some(frame) = notification_rx.recv().await { + if !manager.is_current(generation) { + break; + } + if let Err(error) = notify_message( + ¬ification_config, + notification_connection.clone(), + &frame, + ) + .await + { + eprintln!("failed to create background message notification: {error}"); + } + } + }); + + for frame in prepared.buffered_frames { + handle_push(generation, ¬ification_tx, frame).await; + } + + while manager.is_current(generation) { + match connection.mtp.receive().await { + Ok(frame) => handle_push(generation, ¬ification_tx, frame).await, + Err(error) => { + let _guard = manager.start_lock.lock().expect("start lock poisoned"); + if manager.is_current(generation) { + manager.set_snapshot(MtpSnapshot { + generation, + ready_state: DISCONNECTED, + identified: false, + state: None, + error: Some(error.to_string()), + }); + } + break; + } + } + } + drop(notification_tx); + notification_worker.abort(); + if let Err(error) = notification_worker.await { + if !error.is_cancelled() { + eprintln!("background notification worker failed: {error}"); + } + } + let mut current = manager + .connection + .write() + .expect("connection lock poisoned"); + if current + .as_ref() + .is_some_and(|stored| Arc::ptr_eq(stored, &connection)) + { + current.take(); + } + } + Err(error) => { + let _guard = manager.start_lock.lock().expect("start lock poisoned"); + if manager.is_current(generation) { + manager.set_snapshot(MtpSnapshot { + generation, + ready_state: DISCONNECTED, + identified: false, + state: None, + error: Some(error), + }); + } + } + } + + if !manager.is_current(generation) { + break; + } + android_status("Reconnecting"); + tokio::time::sleep(jittered_retry_delay(delay)).await; + delay = (delay * 2).min(Duration::from_secs(60)); + } +} + +fn jittered_retry_delay(delay: Duration) -> Duration { + let entropy = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.subsec_nanos()) + .unwrap_or_default(); + let percent = 80 + entropy % 41; + delay.mul_f64(percent as f64 / 100.0) +} + +async fn connect(config: &MtpConfig) -> Result { + let (url, public_key) = resolve_endpoint(config) + .await + .map_err(|error| format!("endpoint discovery failed: {error}"))?; + manager().log( + 2, + "Resolved Omikron endpoint", + Some(Value::String(url.clone())), + ); + let keyring_bytes = decode_browser_base64(&config.keyring) + .map_err(|error| format!("invalid MTP keyring: {error}"))?; + let keyring = Keyring::from_bytes(&keyring_bytes) + .map_err(|error| format!("invalid MTP keyring: {error}"))?; + let host_key_bytes = decode_sdk_bytes(&public_key) + .map_err(|error| format!("invalid Omikron public key: {error}"))?; + let host_key = PublicKeyBundle::from_bytes(&host_key_bytes) + .map_err(|error| format!("invalid Omikron public key: {error}"))?; + host_key + .validate() + .map_err(|error| format!("invalid Omikron public key: {error}"))?; + let client_config = ClientConfig::new(url) + .with_client_id(config.user_id) + .with_description("client") + .with_policy(Policy::default().with_send_mode(SendMode::SingleStreamPerMessage)) + .with_ping_interval(Duration::from_secs(30)) + .with_max_missed_pings(3); + #[cfg(target_os = "android")] + let client_config = client_config.with_pinned_pem(android_root_certificates().clone()); + 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 (state, buffered_frames) = await_initial_state(&connection).await?; + let request_ids = RequestIdAllocator::new(); + let (initial_state, ack) = prepare_initial_state_ack(&state, &request_ids)?; + let response = tokio::time::timeout(INITIAL_SYNC_TIMEOUT, 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() + .is_some_and(|name| name.starts_with("Error")) + { + return Err(format!("ClientStateAck failed: {response}")); + } + Ok(PreparedConnection { + connection, + request_ids, + initial_state, + buffered_frames, + }) +} + +fn prepare_initial_state_ack( + state: &CommunicationValue, + request_ids: &RequestIdAllocator, +) -> Result<(Value, CommunicationValue), String> { + let initial_state = frame_data_to_json(state)?; + validate_client_state_sync(&initial_state)?; + let data = initial_state + .as_object() + .ok_or("ClientStateSync payload is not an object")?; + let session_id = required_integer(data, "SessionId")?; + let version = required_integer(data, "VersionNumber")?; + let ack = CommunicationValue::new(communication_type("ClientStateAck")?) + .with_id(request_ids.next()?) + .add_typed_default(DataType::SessionId, number_to_data(session_id)) + .add_typed_default(DataType::VersionNumber, number_to_data(version)); + Ok((initial_state, ack)) +} + +fn validate_client_state_sync(state: &Value) -> Result<(), String> { + let data = state + .as_object() + .ok_or("ClientStateSync payload is not an object")?; + if required_integer(data, "SessionId")? <= 0 { + return Err("ClientStateSync SessionId must be a positive integer".into()); + } + for field in ["VersionNumber", "CacheSchemaVersion"] { + if required_integer(data, field)? < 0 { + return Err(format!("ClientStateSync {field} must be nonnegative")); + } + } + match data.get("SyncMode").and_then(Value::as_str) { + Some("full" | "delta") => {} + _ => return Err("ClientStateSync SyncMode must be 'full' or 'delta'".into()), + } + for field in ["Contacts", "Communities", "Calls", "Messages"] { + if !data.get(field).is_some_and(Value::is_array) { + return Err(format!("ClientStateSync {field} must be an array")); + } + } + for field in ["DeletedMessageIds", "DeletedContactIds"] { + if let Some(value) = data.get(field) { + let values = value + .as_array() + .ok_or_else(|| format!("ClientStateSync {field} must be an array"))?; + if values.iter().any(|value| !value.is_number()) { + return Err(format!("ClientStateSync {field} must contain numbers")); + } + } + } + validate_object_array(data, "Communities", |_| Ok(()))?; + validate_object_array(data, "Contacts", validate_contact)?; + validate_object_array(data, "Calls", validate_call)?; + validate_object_array(data, "Messages", validate_message)?; + Ok(()) +} + +fn validate_object_array( + data: &Map, + field: &str, + validate: impl Fn(&Map) -> Result<(), String>, +) -> Result<(), String> { + let values = data + .get(field) + .and_then(Value::as_array) + .ok_or_else(|| format!("ClientStateSync {field} must be an array"))?; + for value in values { + let object = value + .as_object() + .ok_or_else(|| format!("ClientStateSync {field} entries must be objects"))?; + validate(object)?; + } + Ok(()) +} + +fn validate_contact(contact: &Map) -> Result<(), String> { + if !contact.get("UserId").is_some_and(Value::is_number) { + return Err("ClientStateSync contact omitted numeric UserId".into()); + } + if let Some(messages) = contact.get("Messages") { + let messages = messages + .as_array() + .ok_or("ClientStateSync contact Messages must be an array")?; + for message in messages { + validate_message( + message + .as_object() + .ok_or("ClientStateSync contact message must be an object")?, + )?; + } + } + Ok(()) +} + +fn validate_call(call: &Map) -> Result<(), String> { + if !call.get("CallId").is_some_and(Value::is_string) { + return Err("ClientStateSync call omitted string CallId".into()); + } + let members = call + .get("CallMembers") + .and_then(Value::as_array) + .ok_or("ClientStateSync call omitted CallMembers array")?; + if members.iter().any(|member| !member.is_number()) { + return Err("ClientStateSync CallMembers must contain numbers".into()); + } + Ok(()) +} + +fn validate_message(message: &Map) -> Result<(), String> { + for field in ["SenderId", "SendTime"] { + if !message.get(field).is_some_and(Value::is_number) { + return Err(format!("ClientStateSync message omitted numeric {field}")); + } + } + let content = message + .get("Content") + .and_then(Value::as_str) + .ok_or("ClientStateSync message omitted string Content")?; + STANDARD + .decode(content) + .or_else(|_| STANDARD_NO_PAD.decode(content)) + .map_err(|_| "ClientStateSync message Content must be base64".to_string())?; + if let Some(state) = message.get("MessageState") { + match state.as_str() { + Some("read" | "received" | "sent" | "sending" | "awaiting") => {} + _ => return Err("ClientStateSync message has invalid MessageState".into()), + } + } + Ok(()) +} + +fn required_integer(data: &Map, field: &str) -> Result { + let value = data + .get(field) + .ok_or_else(|| format!("ClientStateSync omitted {field}"))?; + if let Some(value) = value.as_i64() { + return Ok(value as i128); + } + value + .as_u64() + .map(|value| value as i128) + .ok_or_else(|| format!("ClientStateSync {field} must be an integer")) +} + +async fn await_initial_state( + connection: &MTPConnection, +) -> Result<(CommunicationValue, Vec), String> { + let mut buffered = Vec::new(); + let deadline = tokio::time::Instant::now() + INITIAL_SYNC_TIMEOUT; + + loop { + let frame = tokio::time::timeout_at(deadline, connection.receive()) + .await + .map_err(|_| "initial state synchronization timed out".to_string())? + .map_err(|error| format!("initial state synchronization failed: {error}"))?; + + if frame.is_type(CommunicationType::ErrorNoIota) { + return Err("No Iota is currently connected".into()); + } + + if frame.get_type_name() == Some("ClientStateSync") { + return Ok((frame, buffered)); + } + + if buffered.len() == MAX_BUFFERED_INITIAL_FRAMES { + return Err("initial state synchronization buffered too many frames".into()); + } + buffered.push(frame); + } +} + +async fn resolve_endpoint(config: &MtpConfig) -> Result<(String, String), String> { + if let (Some(url), Some(key)) = ( + config.forced_omikron_url.as_ref(), + config.forced_omikron_public_key.as_ref(), + ) { + return Ok((url.clone(), key.clone())); + } + #[derive(Deserialize)] + struct Omikron { + ip_address: String, + port: u16, + public_key: String, + } + let root = config.omega_url.trim_end_matches('/'); + 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() { + return Err(format!("Omikron lookup failed: {}", response.status())); + } + let data: Omikron = response.json().await.map_err(|error| error.to_string())?; + Ok(( + format!("https://{}:{}", data.ip_address, data.port), + data.public_key, + )) +} + +async fn handle_push( + generation: u64, + notification_tx: &mpsc::Sender, + frame: CommunicationValue, +) { + let manager = manager(); + if let Ok(message) = frame_to_json(&frame) { + manager.emit(MtpEvent::Message { + generation, + message, + }); + } + if frame.is_type(CommunicationType::MessageState) + && frame.get_str(DataType::MessageState) == Some("read") + { + if let Some(partner_id) = frame + .get_data(DataType::ChatPartnerId) + .and_then(DataValue::as_number) + .and_then(|value| u64::try_from(value).ok()) + { + if let Err(error) = android_cancel_notification(partner_id) { + eprintln!("failed to clear read message notification: {error}"); + } + } + } + if frame.is_type(CommunicationType::MessageLive) && !manager.ui_visible.load(Ordering::SeqCst) { + if notification_tx.try_send(frame).is_err() { + eprintln!("background message notification queue is full"); + } + } +} + +async fn notify_message( + config: &MtpConfig, + connection: Arc, + frame: &CommunicationValue, +) -> Result<(), String> { + let sender_id = frame + .get_data(DataType::SenderId) + .and_then(DataValue::as_number) + .and_then(|value| u64::try_from(value).ok()) + .ok_or("MessageLive omitted SenderId")?; + let message = frame + .get_data(DataType::Message) + .ok_or("MessageLive omitted Message")?; + let content = container_value_by_name(message, "Content") + .and_then(DataValue::as_str) + .ok_or("MessageLive omitted Content")?; + let keyring_bytes = decode_browser_base64(&config.keyring)?; + let keyring = Keyring::from_bytes(&keyring_bytes).map_err(|error| error.to_string())?; + let chat_id = derive_chat_id(config.user_id, sender_id); + let secret_id = format!("chat:{chat_id}:main"); + let secret_request = CommunicationValue::new(CommunicationType::GetChatSecret) + .with_id(connection.next_request_id().await?) + .add_typed_default(DataType::UserId, DataValue::Str(config.user_id.to_string())) + .add_typed_default(DataType::ChatId, DataValue::Str(chat_id.clone())) + .add_typed_default(DataType::SecretId, DataValue::Str(secret_id.clone())); + let secret = connection + .mtp + .request(&secret_request, None) + .await + .map_err(|error| error.to_string())?; + if !secret.is_type(CommunicationType::ChatSecretResponse) { + return Err(format!("GetChatSecret failed: {secret}")); + } + if secret.get_str(DataType::WrappingScheme) != Some(CHAT_SECRET_SCHEME) { + return Err("unsupported chat secret wrapping scheme".into()); + } + let version = secret + .get_data(DataType::VersionNumber) + .and_then(DataValue::as_number) + .ok_or("missing secret version")?; + let encrypted_secret = secret + .get_bytes(DataType::EncryptedSecret) + .ok_or("missing encrypted secret")?; + let kem_ciphertext = secret + .get_bytes(DataType::KemCiphertext) + .ok_or("missing KEM ciphertext")?; + let shared = HybridKem::decapsulate(&keyring.kem_secret_key, kem_ciphertext) + .map_err(|error| error.to_string())?; + let wrapping_key = derive_encryption_key( + &shared, + CHAT_SECRET_SALT, + format!("{chat_id}:{secret_id}:{version}").as_bytes(), + ) + .map_err(|error| error.to_string())?; + let chat_secret = ChaCha20Poly1305::new(wrapping_key) + .decrypt(encrypted_secret, b"") + .map_err(|error| error.to_string())?; + let message_key = derive_encryption_key(&chat_secret, CHAT_MESSAGE_SALT, b"message-content") + .map_err(|error| error.to_string())?; + let ciphertext = STANDARD + .decode(content) + .map_err(|error| error.to_string())?; + let plaintext = ChaCha20Poly1305::new(message_key) + .decrypt(&ciphertext, b"") + .map_err(|error| error.to_string())?; + + let user_request = CommunicationValue::new(CommunicationType::GetUserData) + .with_id(connection.next_request_id().await?) + .add_typed_default( + DataType::UserId, + DataValue::UnsignedNumber(sender_id as u128), + ); + let user = connection + .mtp + .request(&user_request, None) + .await + .map_err(|error| error.to_string())?; + let sender = user + .get_str(DataType::Display) + .or_else(|| user.get_str(DataType::Username)) + .map(str::to_owned) + .unwrap_or_else(|| format!("User {sender_id}")); + let avatar = user + .get_str(DataType::Avatar) + .and_then(|avatar| decode_browser_base64(avatar).ok()); + android_notify( + sender_id, + &sender, + &String::from_utf8_lossy(&plaintext), + avatar.as_deref(), + )?; + Ok(()) +} + +fn derive_chat_id(own: u64, peer: u64) -> String { + format!("{}:{}", own.min(peer), own.max(peer)) +} + +fn decode_browser_base64(value: &str) -> Result, String> { + let normalized: String = value + .chars() + .filter(|character| !character.is_ascii_whitespace()) + .collect(); + if normalized.is_empty() { + return Err("value is empty".into()); + } + STANDARD + .decode(&normalized) + .or_else(|_| STANDARD_NO_PAD.decode(&normalized)) + .map_err(|_| "invalid base64 encoding".into()) +} + +fn decode_sdk_bytes(value: &str) -> Result, String> { + let trimmed = value.trim(); + let hex: String = trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")) + .unwrap_or(trimmed) + .chars() + .filter(|character| !matches!(character, ' ' | '\t' | '\r' | '\n' | ':' | '_' | '-')) + .collect(); + if !hex.is_empty() && hex.chars().all(|character| character.is_ascii_hexdigit()) { + if hex.len() % 2 != 0 { + return Err("hex string has an odd length".into()); + } + return (0..hex.len()) + .step_by(2) + .map(|offset| { + u8::from_str_radix(&hex[offset..offset + 2], 16) + .map_err(|_| "invalid hex encoding".to_string()) + }) + .collect(); + } + decode_browser_base64(trimmed) +} + +#[cfg(target_os = "android")] +fn android_root_certificates() -> &'static Vec { + static ROOTS: OnceLock> = OnceLock::new(); + ROOTS.get_or_init(|| { + let mut pem = Vec::new(); + for certificate in webpki_root_certs::TLS_SERVER_ROOT_CERTS { + pem.extend_from_slice(b"-----BEGIN CERTIFICATE-----\n"); + let encoded = STANDARD.encode(certificate.as_ref()); + for line in encoded.as_bytes().chunks(64) { + pem.extend_from_slice(line); + pem.push(b'\n'); + } + pem.extend_from_slice(b"-----END CERTIFICATE-----\n"); + } + pem.extend_from_slice(ROOT_YE_PEM); + pem.extend_from_slice(YE1_PEM); + pem + }) +} + +fn container_value(value: &DataValue, field: DataType) -> Option<&DataValue> { + let id = field.try_to_id(&TypeMap::latest())?; + value.get_field(id) +} + +fn container_value_by_name<'a>(value: &'a DataValue, field: &str) -> Option<&'a DataValue> { + container_value(value, DataType::from_name(field)?) +} + +fn communication_type(name: &str) -> Result { + CommunicationType::from_name(name).ok_or_else(|| format!("unknown communication type: {name}")) +} + +fn json_to_frame(type_name: &str, data: Value, id: u32) -> Result { + let comm_type = communication_type(type_name)?; + let mut frame = CommunicationValue::new(comm_type).with_id(id); + let Value::Object(fields) = data else { + return Err("MTP request data must be an object".into()); + }; + for (name, value) in fields { + let data_type = + DataType::from_name(&name).ok_or_else(|| format!("unknown data type: {name}"))?; + frame = frame.add_typed_default(data_type, json_to_data(&name, value)?); + } + Ok(frame) +} + +fn json_to_data(field: &str, value: Value) -> Result { + Ok(match value { + Value::Null => DataValue::Null, + Value::Bool(value) => DataValue::from(value), + Value::Number(value) => { + if let Some(integer) = value.as_u64() { + DataValue::UnsignedNumber(integer as u128) + } else if let Some(integer) = value.as_i64() { + DataValue::SignedNumber(integer as i128) + } else { + DataValue::Float(value.as_f64().ok_or("invalid number")?) + } + } + Value::String(value) => DataValue::Str(value), + Value::Array(values) if is_bytes_field(field) => DataValue::Bytes( + values + .into_iter() + .map(|value| { + value + .as_u64() + .and_then(|n| u8::try_from(n).ok()) + .ok_or("invalid byte") + }) + .collect::, _>>()?, + ), + Value::Array(values) => DataValue::Array( + values + .into_iter() + .map(|value| json_to_data(field, value)) + .collect::>()?, + ), + Value::Object(fields) => { + let mut entries = Vec::with_capacity(fields.len()); + for (name, value) in fields { + let data_type = DataType::from_name(&name) + .ok_or_else(|| format!("unknown nested data type: {name}"))?; + let id = data_type + .try_to_id(&TypeMap::latest()) + .ok_or_else(|| format!("unmapped data type: {name}"))?; + entries.push((id, json_to_data(&name, value)?)); + } + DataValue::Container(entries) + } + }) +} + +fn number_to_data(value: i128) -> DataValue { + if value >= 0 { + DataValue::UnsignedNumber(value as u128) + } else { + DataValue::SignedNumber(value) + } +} + +fn is_bytes_field(field: &str) -> bool { + matches!( + field, + "EncryptedSecret" | "KemCiphertext" | "Payload" | "PublicKeys" + ) +} + +fn frame_to_json(frame: &CommunicationValue) -> Result { + let mut result = Map::new(); + if let Some(id) = frame.id() { + result.insert("id".into(), Value::from(id)); + } + result.insert( + "type".into(), + Value::String(frame.get_type_name().unwrap_or("Unknown").to_owned()), + ); + result.insert("data".into(), frame_data_to_json(frame)?); + Ok(Value::Object(result)) +} + +fn frame_data_to_json(frame: &CommunicationValue) -> Result { + let map = frame.type_map().cloned().unwrap_or_else(TypeMap::latest); + let mut result = Map::new(); + let entries = frame + .data() + .ok_or("MTP frame payload is not a data container")?; + for (id, value) in entries { + let name = map + .data_type_name(id.0) + .ok_or_else(|| format!("unknown data type id: {}", id.0))?; + result.insert(name.to_owned(), data_to_json(value, &map)?); + } + Ok(Value::Object(result)) +} + +fn data_to_json(value: &DataValue, map: &TypeMap) -> Result { + Ok(match value { + DataValue::BoolTrue => Value::Bool(true), + DataValue::BoolFalse => Value::Bool(false), + DataValue::Bool(value) => Value::Bool(*value), + DataValue::SignedNumber(value) => number_to_json(*value)?, + DataValue::UnsignedNumber(value) => { + number_to_json(i128::try_from(*value).map_err(|_| "number exceeds JSON range")?)? + } + DataValue::Float(value) => serde_json::Number::from_f64(*value) + .map(Value::Number) + .ok_or("invalid float")?, + DataValue::Str(value) => Value::String(value.clone()), + DataValue::Bytes(value) => Value::Array(value.iter().copied().map(Value::from).collect()), + DataValue::Array(values) => Value::Array( + values + .iter() + .map(|value| data_to_json(value, map)) + .collect::>()?, + ), + DataValue::Container(entries) => { + let mut object = Map::new(); + for (id, value) in entries { + let name = map + .data_type_name(id.0) + .ok_or_else(|| format!("unknown nested data type id: {}", id.0))?; + object.insert(name.to_owned(), data_to_json(value, map)?); + } + Value::Object(object) + } + DataValue::Null => Value::Null, + _ => return Err("encrypted protocol values cannot cross the frontend bridge".into()), + }) +} + +fn number_to_json(value: i128) -> Result { + i64::try_from(value) + .map(Value::from) + .map_err(|_| "number exceeds JSON range".into()) +} + +#[cfg(test)] +mod tests { + use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; + use serde_json::json; + + use super::{ + container_value_by_name, decode_browser_base64, decode_sdk_bytes, frame_to_json, + jittered_retry_delay, json_to_frame, prepare_initial_state_ack, RequestIdAllocator, + }; + + #[test] + fn browser_base64_accepts_file_whitespace_and_missing_padding() { + assert_eq!(decode_browser_base64(" AQID\r\n").unwrap(), [1, 2, 3]); + assert_eq!(decode_browser_base64("AQI").unwrap(), [1, 2]); + } + + #[test] + fn sdk_bytes_accept_hex_and_base64() { + assert_eq!(decode_sdk_bytes("0x01:02-ff").unwrap(), [1, 2, 255]); + assert_eq!(decode_sdk_bytes("AQI=").unwrap(), [1, 2]); + } + + #[test] + fn request_ids_are_nonzero_and_monotonic() { + let ids = RequestIdAllocator::new(); + + assert_eq!(ids.next().unwrap(), 1); + assert_eq!(ids.next().unwrap(), 2); + } + + #[test] + fn retry_jitter_stays_within_policy_bounds() { + let delay = jittered_retry_delay(std::time::Duration::from_secs(10)); + + assert!(delay >= std::time::Duration::from_secs(8)); + assert!(delay <= std::time::Duration::from_secs(12)); + } + + #[test] + fn json_content_uses_content_wire_type() { + let frame = json_to_frame( + "MessageEdit", + json!({ + "Content": "ciphertext", + "ChatPartnerId": 42, + "SendTime": 10, + }), + 1, + ) + .unwrap(); + + assert_eq!( + frame + .get_data(DataType::Content) + .and_then(DataValue::as_str), + Some("ciphertext") + ); + } + + #[test] + fn nested_json_content_uses_content_wire_type() { + let frame = json_to_frame( + "MessageEdit", + json!({ + "Message": { "Content": "ciphertext" }, + }), + 1, + ) + .unwrap(); + let message = frame.get_data(DataType::Message).unwrap(); + + assert_eq!( + container_value_by_name(message, "Content").and_then(DataValue::as_str), + Some("ciphertext") + ); + } + + #[test] + fn content_is_exposed_to_frontend() { + let frame = CommunicationValue::new(CommunicationType::MessageEditLive) + .with_id(1) + .add_typed_default(DataType::Content, DataValue::Str("ciphertext".into())); + + let json = frame_to_json(&frame).unwrap(); + + assert_eq!(json["data"]["Content"], "ciphertext"); + } + + fn valid_initial_state() -> CommunicationValue { + CommunicationValue::new(communication_type("ClientStateSync").unwrap()) + .add_typed_default(DataType::SessionId, DataValue::UnsignedNumber(1)) + .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::SyncMode, DataValue::Str("full".into())) + .add_typed_default(DataType::Contacts, DataValue::Array(vec![])) + .add_typed_default(DataType::Communities, DataValue::Array(vec![])) + .add_typed_default(DataType::Calls, DataValue::Array(vec![])) + .add_typed_default(DataType::Messages, DataValue::Array(vec![])) + } + + #[test] + fn valid_initial_state_is_prepared_before_ack() { + let ids = RequestIdAllocator::new(); + let (state, ack) = prepare_initial_state_ack(&valid_initial_state(), &ids).unwrap(); + + assert_eq!(state["SyncMode"], "full"); + assert_eq!(ack.get_type_name(), Some("ClientStateAck")); + assert_eq!(ack.id(), Some(1)); + } + + #[test] + fn malformed_initial_state_does_not_prepare_ack() { + let ids = RequestIdAllocator::new(); + let malformed = valid_initial_state() + .add_typed_default(DataType::SyncMode, DataValue::Str("invalid".into())); + + assert!(prepare_initial_state_ack(&malformed, &ids).is_err()); + assert_eq!( + ids.next().unwrap(), + 1, + "no acknowledgement ID was allocated" + ); + } + + #[test] + fn malformed_nested_initial_state_does_not_prepare_ack() { + let ids = RequestIdAllocator::new(); + let malformed = CommunicationValue::new(communication_type("ClientStateSync").unwrap()) + .add_typed_default(DataType::SessionId, DataValue::UnsignedNumber(1)) + .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::SyncMode, DataValue::Str("full".into())) + .add_typed_default( + DataType::Contacts, + DataValue::Array(vec![DataValue::Container(vec![])]), + ) + .add_typed_default(DataType::Communities, DataValue::Array(vec![])) + .add_typed_default(DataType::Calls, DataValue::Array(vec![])) + .add_typed_default(DataType::Messages, DataValue::Array(vec![])); + + assert!(prepare_initial_state_ack(&malformed, &ids).is_err()); + assert_eq!( + ids.next().unwrap(), + 1, + "no acknowledgement ID was allocated" + ); + } +} + +#[tauri::command] +pub async fn mtp_request(type_name: String, data: Value) -> Result { + manager().request(&type_name, data).await +} + +#[tauri::command] +pub fn mtp_status() -> MtpSnapshot { + manager().snapshot() +} + +#[tauri::command] +pub fn mtp_store_credentials(config: MtpConfig) -> Result<(), String> { + let serialized = serde_json::to_string(&config).map_err(|error| error.to_string())?; + android_store_config(&serialized)?; + *manager().config.write().expect("config lock poisoned") = Some(config); + Ok(()) +} + +#[tauri::command] +pub fn mtp_has_credentials() -> Result { + android_has_config() +} + +#[tauri::command] +pub fn mtp_load_keyring() -> Option { + manager() + .config + .read() + .expect("config lock poisoned") + .as_ref() + .map(|config| config.keyring.clone()) +} + +#[tauri::command] +pub fn mtp_set_enabled(enabled: bool) -> Result<(), String> { + android_set_enabled(enabled)?; + if !enabled { + manager().stop(); + } + Ok(()) +} + +#[tauri::command] +pub fn mtp_is_ignoring_battery_optimizations() -> Result { + android_is_ignoring_battery_optimizations() +} + +#[tauri::command] +pub fn mtp_request_battery_exemption() -> Result<(), String> { + android_request_battery_exemption() +} + +#[tauri::command] +pub fn mtp_set_ui_visible(visible: bool) { + manager().set_ui_visible(visible); +} + +#[tauri::command] +pub fn mtp_post_message_notification( + sender_id: u64, + sender: String, + body: String, + avatar: Option, +) -> Result { + #[cfg(target_os = "android")] + { + let avatar = avatar.as_deref().map(decode_browser_base64).transpose()?; + android_notify(sender_id, &sender, &body, avatar.as_deref())?; + Ok(true) + } + #[cfg(not(target_os = "android"))] + { + let _ = (sender_id, sender, body, avatar); + Ok(false) + } +} + +#[cfg(not(target_os = "android"))] +fn android_store_config(_: &str) -> Result<(), String> { + Ok(()) +} +#[cfg(not(target_os = "android"))] +fn android_has_config() -> Result { + Ok(manager() + .config + .read() + .expect("config lock poisoned") + .is_some()) +} +#[cfg(not(target_os = "android"))] +fn android_set_enabled(_: bool) -> Result<(), String> { + Ok(()) +} +#[cfg(not(target_os = "android"))] +fn android_status(_: &str) {} +#[cfg(not(target_os = "android"))] +fn android_notify(_: u64, _: &str, _: &str, _: Option<&[u8]>) -> Result<(), String> { + Ok(()) +} +#[cfg(not(target_os = "android"))] +fn android_cancel_notification(_: u64) -> Result<(), String> { + Ok(()) +} +#[cfg(not(target_os = "android"))] +fn android_is_ignoring_battery_optimizations() -> Result { + Ok(true) +} +#[cfg(not(target_os = "android"))] +fn android_request_battery_exemption() -> Result<(), String> { + Ok(()) +} + +#[cfg(target_os = "android")] +mod android { + use std::sync::OnceLock; + + use jni::{ + jni_sig, jni_str, + objects::{Global, JClass, JObject, JString, JValue}, + Env, EnvUnowned, JavaVM, + }; + use serde_json::Value; + + pub struct Host { + vm: JavaVM, + context: Global>, + bridge: Global>, + } + + static HOST: OnceLock = OnceLock::new(); + + pub 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/NativeMtpBridge")) + .map_err(|e| e.to_string())?; + let bridge = env + .get_static_field( + class, + jni_str!("INSTANCE"), + jni_sig!("Lnet/tensamin/client/NativeMtpBridge;"), + ) + .and_then(|value| value.l()) + .map_err(|e| e.to_string())?; + HOST.set(Host { + vm: env.get_java_vm().map_err(|e| e.to_string())?, + context: env.new_global_ref(context).map_err(|e| e.to_string())?, + bridge: env.new_global_ref(bridge).map_err(|e| e.to_string())?, + }) + .map_err(|_| "Android MTP host is already attached".to_string()) + } + + fn with_env(call: impl FnOnce(&mut Env, &Host) -> Result) -> Result { + let host = HOST.get().ok_or("Android MTP host is not attached")?; + host.vm + .attach_current_thread(|env| Ok::<_, jni::errors::Error>(call(env, host))) + .map_err(|e| e.to_string())? + } + + pub fn store_config(config: &str) -> Result<(), String> { + with_env(|env, host| { + let value = env.new_string(config).map_err(|e| e.to_string())?; + env.call_method( + host.bridge.as_obj(), + jni_str!("storeConfig"), + jni_sig!("(Landroid/content/Context;Ljava/lang/String;)V"), + &[ + JValue::Object(host.context.as_obj()), + JValue::Object(&value), + ], + ) + .map_err(|e| e.to_string())?; + Ok(()) + }) + } + + pub fn has_config() -> Result { + with_env(|env, host| { + env.call_method( + host.bridge.as_obj(), + jni_str!("hasConfig"), + jni_sig!("(Landroid/content/Context;)Z"), + &[JValue::Object(host.context.as_obj())], + ) + .and_then(|value| value.z()) + .map_err(|e| e.to_string()) + }) + } + + pub fn set_enabled(enabled: bool) -> Result<(), String> { + with_env(|env, host| { + env.call_method( + host.bridge.as_obj(), + jni_str!("setServiceEnabled"), + jni_sig!("(Landroid/content/Context;Z)V"), + &[ + JValue::Object(host.context.as_obj()), + JValue::Bool(enabled.into()), + ], + ) + .map_err(|e| e.to_string())?; + Ok(()) + }) + } + + pub fn status(status: &str) { + let _ = with_env(|env, host| { + let status = env.new_string(status).map_err(|e| e.to_string())?; + env.call_method( + host.bridge.as_obj(), + jni_str!("updateServiceStatus"), + jni_sig!("(Landroid/content/Context;Ljava/lang/String;)V"), + &[ + JValue::Object(host.context.as_obj()), + JValue::Object(&status), + ], + ) + .map_err(|e| e.to_string())?; + Ok(()) + }); + } + + pub fn notify( + sender_id: u64, + sender: &str, + body: &str, + avatar: Option<&[u8]>, + ) -> Result<(), String> { + with_env(|env, host| { + let sender = env.new_string(sender).map_err(|e| e.to_string())?; + let body = env.new_string(body).map_err(|e| e.to_string())?; + let avatar = env + .byte_array_from_slice(avatar.unwrap_or_default()) + .map_err(|e| e.to_string())?; + env.call_method( + host.bridge.as_obj(), + jni_str!("postMessageNotification"), + jni_sig!("(Landroid/content/Context;JLjava/lang/String;Ljava/lang/String;[B)V"), + &[ + JValue::Object(host.context.as_obj()), + JValue::Long(sender_id as i64), + JValue::Object(&sender), + JValue::Object(&body), + JValue::Object(&avatar), + ], + ) + .map_err(|e| e.to_string())?; + Ok(()) + }) + } + + pub fn cancel_notification(sender_id: u64) -> Result<(), String> { + with_env(|env, host| { + env.call_method( + host.bridge.as_obj(), + jni_str!("cancelMessageNotification"), + jni_sig!("(Landroid/content/Context;J)V"), + &[ + JValue::Object(host.context.as_obj()), + JValue::Long(sender_id as i64), + ], + ) + .map_err(|e| e.to_string())?; + Ok(()) + }) + } + + pub fn is_ignoring_battery_optimizations() -> Result { + with_env(|env, host| { + env.call_method( + host.bridge.as_obj(), + jni_str!("isIgnoringBatteryOptimizations"), + jni_sig!("(Landroid/content/Context;)Z"), + &[JValue::Object(host.context.as_obj())], + ) + .and_then(|value| value.z()) + .map_err(|e| e.to_string()) + }) + } + + pub fn request_battery_exemption() -> Result<(), String> { + with_env(|env, host| { + env.call_method( + host.bridge.as_obj(), + jni_str!("requestBatteryExemption"), + jni_sig!("(Landroid/content/Context;)V"), + &[JValue::Object(host.context.as_obj())], + ) + .map_err(|e| e.to_string())?; + Ok(()) + }) + } + + #[no_mangle] + pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeAttach<'caller>( + mut env: EnvUnowned<'caller>, + _class: JClass, + context: JObject<'caller>, + ) { + let _ = env.with_env(|env| { + let _ = attach(env, context); + Ok::<_, jni::errors::Error>(()) + }); + } + + #[no_mangle] + pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeStart<'caller>( + mut env: EnvUnowned<'caller>, + _class: JClass, + config: JString<'caller>, + ) { + let _ = env.with_env(|env| { + if let Ok(config) = config.mutf8_chars(env) { + if let Ok(config) = serde_json::from_str(config.to_str().as_ref()) { + super::manager().configure_and_start(config); + } + } + Ok::<_, jni::errors::Error>(()) + }); + } + + #[no_mangle] + pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeStop( + _env: EnvUnowned, + _class: JClass, + ) { + let _ = std::panic::catch_unwind(|| super::manager().stop()); + } + + #[no_mangle] + pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeSetUiState( + _env: EnvUnowned, + _class: JClass, + visible: jni::sys::jboolean, + ) { + super::manager().set_ui_visible(visible); + } + + #[no_mangle] + pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeLog<'caller>( + mut env: EnvUnowned<'caller>, + _class: JClass, + level: jni::sys::jint, + message: JString<'caller>, + details: JString<'caller>, + ) { + let _ = env.with_env(|env| { + if let (Ok(message), Ok(details)) = (message.mutf8_chars(env), details.mutf8_chars(env)) + { + let message = message.to_str(); + let details = details.to_str(); + super::manager().log( + level.clamp(0, 3) as u8, + message.into_owned(), + (!details.is_empty()).then(|| Value::String(details.into_owned())), + ); + } + Ok::<_, jni::errors::Error>(()) + }); + } +} + +#[cfg(target_os = "android")] +use android::{ + cancel_notification as android_cancel_notification, has_config as android_has_config, + is_ignoring_battery_optimizations as android_is_ignoring_battery_optimizations, + notify as android_notify, request_battery_exemption as android_request_battery_exemption, + set_enabled as android_set_enabled, status as android_status, + store_config as android_store_config, +}; diff --git a/apps/tauri/src/deeplinkHandler.tsx b/apps/tauri/src/deeplinkHandler.tsx index 2094b73..dcbb5c7 100644 --- a/apps/tauri/src/deeplinkHandler.tsx +++ b/apps/tauri/src/deeplinkHandler.tsx @@ -9,11 +9,11 @@ import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"; import { isTauri } from "@tauri-apps/api/core"; import { useIsMobile } from "@methanium/ui"; -type DeeplinkContextValue = { - deeplinks: readonly string[]; -}; -export const deeplinkContext = createContext( + +export const deeplinkContext = createContext<{ + deeplinks: readonly string[]; +} | undefined>( undefined, ); diff --git a/apps/tauri/src/qrCodeScanner.tsx b/apps/tauri/src/qrCodeScanner.tsx deleted file mode 100644 index fa3f3c7..0000000 --- a/apps/tauri/src/qrCodeScanner.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { - scan, - Format, - requestPermissions, -} from "@tauri-apps/plugin-barcode-scanner"; -import { Button } from "@methanium/ui"; - -import { toast } from "@tensamin/shared/log"; - -export default function QrCodeScanner({ - onData, -}: { - onData: (data: string) => void; -}) { - return ( - - ); -} diff --git a/apps/web/index.html b/apps/web/index.html index eef61d9..0c0644f 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -2,8 +2,11 @@ - - + + Tensamin diff --git a/apps/web/package.json b/apps/web/package.json index d95d914..a6b3951 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,140 +6,58 @@ "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", + "build": "pnpm --filter @tensamin/pwa build && pnpm run test && tsc -b && vite build", "preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .." }, "dependencies": { - "@base-ui/react": "^1.0.0", - "@base-ui/utils": "0.3.1", - "@babel/runtime": "^7.29.2", - "@fontsource-variable/inter": "^5.2.8", - "@fontsource-variable/public-sans": "^5.2.7", - "@floating-ui/core": "^1.7.0", - "@floating-ui/dom": "^1.7.0", - "@floating-ui/react-dom": "^2.1.8", - "@floating-ui/utils": "^0.2.11", - "@radix-ui/primitive": "^1.1.0", - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-context": "^1.1.4", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dismissable-layer": "^1.1.11", - "@radix-ui/react-focus-guards": "^1.1.4", - "@radix-ui/react-focus-scope": "^1.1.11", - "@radix-ui/react-id": "^1.1.0", - "@radix-ui/react-portal": "^1.1.13", - "@radix-ui/react-presence": "^1.1.6", - "@radix-ui/react-primitive": "^2.0.2", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-use-callback-ref": "^1.1.1", - "@radix-ui/react-use-controllable-state": "^1.2.3", - "@radix-ui/react-use-effect-event": "^0.0.2", - "@radix-ui/react-use-layout-effect": "^1.1.0", - "@reduxjs/toolkit": "^2.0.0", - "@tailwindcss/vite": "^4.2.4", - "@tanstack/devtools-event-client": "^0.3.0", - "@tanstack/query-core": "^5.0.0", - "@tanstack/react-router": "^1.169.1", - "@tanstack/history": "1.162.0", - "@tanstack/react-store": "^0.9.3", - "@tanstack/router-core": "^1.169.1", - "@tanstack/store": "^0.9.3", - "@tanstack/virtual-core": "^3.13.24", - "@tanstack/react-virtual": "^3.13.24", - "@tauri-apps/api": "^2", - "@tensamin/call": "workspace:*", + "@fontsource-variable/public-sans": "^5.3.0", + "@methanium/ui": "*", + "@tailwindcss/vite": "^4.3.3", + "@tanstack/react-router": "^1.170.21", + "@tanstack/react-virtual": "^3.14.9", + "@tauri-apps/api": "^2.11.1", + "@tensamin/pwa": "workspace:*", "@tensamin/cache": "workspace:*", + "@tensamin/call": "workspace:*", "@tensamin/chat": "workspace:*", "@tensamin/crypto": "workspace:*", - "@tensamin/shared": "workspace:*", - "@tensamin/settings": "workspace:*", - "@tensamin/storage": "workspace:*", - "@tensamin/tauri": "workspace:*", + "@tensamin/hotkeys": "workspace:*", "@tensamin/mtp": "workspace:*", - "@tensamin/tauth": "workspace:*", - "@tensamin/markdown": "workspace:*", - "@methanium/ui": "*", - "@tensamin/user": "workspace:*", "@tensamin/notifications": "workspace:*", "@tensamin/onboarding": "workspace:*", - "aria-hidden": "^1.2.4", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "cookie-es": "^3.0.0", - "d3-array": "^3.1.6", - "d3-color": "^3.1.0", - "d3-ease": "^3.0.1", - "d3-format": "^3.1.0", - "d3-interpolate": "^3.0.1", - "d3-path": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-time-format": "^4.1.0", - "d3-timer": "^3.0.1", - "date-fns": "^4.4.0", + "@tensamin/settings": "workspace:*", + "@tensamin/shared": "workspace:*", + "@tensamin/storage": "workspace:*", + "@tensamin/tauri": "workspace:*", + "@tensamin/tauth": "workspace:*", + "@tensamin/user": "workspace:*", "decimal.js-light": "^2.5.1", - "detect-node-es": "^1.1.0", - "dijkstrajs": "^1.0.1", - "embla-carousel": "8.6.0", - "embla-carousel-react": "^8.6.0", - "embla-carousel-reactive-utils": "8.6.0", - "es-toolkit": "^1.39.3", - "eventemitter3": "^5.0.1", - "get-nonce": "^1.0.1", - "immer": "^10.1.1", - "input-otp": "^1.4.2", - "internmap": "^2.0.3", - "tw-animate-css": "^1.4.0", - "lucide-react": "^1.14.0", - "next-themes": "^0.4.6", - "pngjs": "^5.0.0", - "qrcode": "^1.5.4", - "react": "^19.2.0", - "react-day-picker": "^10.0.1", - "react-dom": "^19.2.0", - "react-is": "^19.0.0", - "react-redux": "^9.0.0", - "react-remove-scroll": "^2.7.2", - "react-remove-scroll-bar": "^2.3.7", - "react-resizable-panels": "^4.11.2", - "react-style-singleton": "^2.2.3", - "recharts": "3.8.1", - "redux": "^5.0.0", - "redux-thunk": "^3.1.0", - "reselect": "5.1.1", - "scheduler": "^0.27.0", - "seroval": "^1.5.4", - "seroval-plugins": "^1.5.4", - "shadcn": "^4.11.0", - "sonner": "^2.0.7", - "tailwindcss": "^4.2.4", - "tailwind-merge": "^3.6.0", + "eventemitter3": "^5.0.4", + "lucide-react": "^1.29.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-is": "^19.2.8", + "react-redux": "^9.3.0", "tailwind-scrollbar-hide": "^4.0.0", - "tiny-invariant": "^1.3.3", - "tslib": "^2.8.1", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3", - "use-sync-external-store": "^1.2.2", - "vaul": "^1.1.2", - "victory-vendor": "^37.0.2", - "yargs": "^15.3.1", - "zod": "^4.3.6" + "tailwindcss": "^4.3.3", + "tw-animate-css": "^1.4.0", + "use-sync-external-store": "^1.6.0", + "zod": "^4.4.3" }, "devDependencies": { "@eslint/js": "^10.0.1", "@types/qrcode": "^1.5.6", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^6.0.1", - "esbuild": "^0.25.11", - "eslint": "^10.0.3", - "globals": "^17.4.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.0.5", + "esbuild": "^0.28.1", + "eslint": "^10.8.0", + "globals": "^17.9.0", + "mtp": "*", "typescript": "~6.0.3", - "typescript-eslint": "^8.57.0", - "vite": "^8.0.10" + "typescript-eslint": "^8.66.0", + "vite": "^8.2.1" } } diff --git a/apps/web/src/components/modals/basic.tsx b/apps/web/src/components/modals/basic.tsx index 46eaa45..dc4e48a 100644 --- a/apps/web/src/components/modals/basic.tsx +++ b/apps/web/src/components/modals/basic.tsx @@ -15,7 +15,10 @@ export function Basic({ user, extra, }: { - user: User; + user: Pick< + User, + "Display" | "Username" | "Avatar" | "OnlineStatus" | "Status" + >; extra?: React.ReactNode; }) { const display = user.Display || user.Username || "Unknown"; @@ -57,8 +60,9 @@ export function Basic({ -
+

{display}

+

{user.Status}

{extra}
diff --git a/apps/web/src/components/modals/profile.tsx b/apps/web/src/components/modals/profile.tsx index edd83a6..a4bddb2 100644 --- a/apps/web/src/components/modals/profile.tsx +++ b/apps/web/src/components/modals/profile.tsx @@ -1,10 +1,23 @@ import type { User } from "@tensamin/user/context"; import { Avatar, AvatarFallback, AvatarImage, Button } from "@methanium/ui"; -import Text from "@tensamin/markdown/text"; +import { Text } from "@methanium/ui/markdown"; import { ChevronDown, ChevronUp } from "lucide-react"; import { useState } from "react"; -export default function Profile({ user }: { user: User }) { +export default function Profile({ + user, +}: { + user: Pick< + User, + | "UserId" + | "Display" + | "Username" + | "Avatar" + | "About" + | "IotaId" + | "PublicKey" + >; +}) { const [showAdvancedInformation, setShowAdvancedInformation] = useState(false); return ( diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index bafdfa8..122f188 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -5,7 +5,14 @@ import { PopoverTrigger, useIsMobile, } from "@methanium/ui"; -import { ArrowLeft, House, Phone, Settings, User } from "lucide-react"; +import { + ArrowLeft, + EllipsisVertical, + House, + Phone, + Settings, + User, +} from "lucide-react"; import { useLocation, useNavigate, useSearch } from "@tanstack/react-router"; import { joinCall, useCall } from "@tensamin/call/store"; import Wrapper from "@tensamin/user/wrapper"; @@ -40,9 +47,12 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { const [userInfoOpen, setUserInfoOpen] = useState(false); + const { callId } = useCall(); + return (
@@ -76,9 +86,21 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { )} + {isMobile && pathname === "/call" && callId && ( +

{displayCallId(callId)}

+ )} {pathname === "/chat" && id && ( isMobile ? (

{user?.Display}

@@ -172,6 +194,11 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { )} )} + {isMobile && pathname === "/call" && callId && ( + + )}
diff --git a/apps/web/src/components/screens/login/form.tsx b/apps/web/src/components/screens/login/form.tsx index c81ef92..c9fa041 100644 --- a/apps/web/src/components/screens/login/form.tsx +++ b/apps/web/src/components/screens/login/form.tsx @@ -13,8 +13,11 @@ import { useState, } from "react"; import { z } from "zod"; -import { isTauri } from "@tauri-apps/api/core"; -import QrCodeScanner from "@tensamin/tauri/qrCodeScanner"; +import { subscribeTuFileLaunch } from "@tensamin/pwa/runtime"; +import { + parseTuFileContent, + persistMtpCredentials, +} from "@tensamin/storage/credentials"; import { useNavigate } from "@tanstack/react-router"; const fetchedUser = z.object({ @@ -35,59 +38,11 @@ const formSchema = z.object({ mtp_keyring: z.string().min(1).max(92), }); -/** - * Parses a .tu file payload into credentials. - * @param rawFileContent UTF-8 file content from an uploaded .tu file. - * @returns Parsed user id and private key credentials. - */ -function parseTuFileContent(rawFileContent: string): { - userId: number; - privateKey: string; - domain: string | null; -} { - if (rawFileContent.trim().length === 0) { - throw new Error("File is empty"); - } else if (!rawFileContent.includes("::")) { - throw new Error("Invalid file"); - } else if (rawFileContent.split("::").length !== 2) { - throw new Error("Invalid file"); - } - - const left = rawFileContent.split("::")[0]; - const right = rawFileContent.split("::")[1]; - - if (left.length === 0) { - throw new Error("Invalid file"); - } else if (right.length === 0) { - throw new Error("Invalid file"); - } else if (isNaN(Number(left)) && !left.includes("@")) { - throw new Error("Invalid file"); - } - - const [userIdString, privateKey] = rawFileContent.split("::"); - const userId = isNaN(Number(userIdString)) - ? Number(userIdString.split("@")[0]) - : Number(userIdString); - - const domain = userIdString.includes("@") ? userIdString.split("@")[1] : null; - - if (!userId || !privateKey) { - throw new Error("Invalid file"); - } - - console.log({ - domain, - userId, - }); - - return { userId, privateKey, domain }; -} - export default function Form() { const isMobile = useIsMobile(); const uploadRef = useRef(null); const [isDragging, setIsDragging] = useState(false); - const { save } = useStorage(); + const { load, save } = useStorage(); const navigate = useNavigate(); const loginPendingRef = useRef(false); @@ -96,17 +51,19 @@ export default function Form() { if (loginPendingRef.current) return false; loginPendingRef.current = true; try { - if (domain) await save("omega_url", `https://${domain}/`); - await save("mtp_keyring", privateKey, { secure: true }); - await save("session_id", Date.now()); - await save("user_id", userId); + await persistMtpCredentials({ + storage: { load, save }, + userId, + keyring: privateKey, + domain, + }); await navigate({ to: "/", replace: true }); return true; } finally { loginPendingRef.current = false; } }, - [navigate, save], + [load, navigate, save], ); // Process dropped files @@ -130,6 +87,11 @@ export default function Form() { [persistLogin], ); + useEffect( + () => subscribeTuFileLaunch(processDroppedFile), + [processDroppedFile], + ); + // Handle .tu files const handleFileInputChange = useCallback( async (event: ChangeEvent): Promise => { @@ -268,32 +230,10 @@ export default function Form() { return (
- {isTauri() && isMobile ? ( - <> - { - if (!data.startsWith("tensamin://tu::")) { - toast("error", "Invalid QR code"); - return; - } - - const decoded = data.replace("tensamin://tu::", ""); - - try { - const { userId, privateKey, domain } = - parseTuFileContent(decoded); - - await persistLogin(userId, privateKey, domain); - } catch (error) { - log(0, "login", "red", error); - toast("error", "Failed to parse QR code data"); - } - }} - /> - - + {isMobile ? ( + ) : (
uploadRef.current?.click()} diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx index 10baf34..d56a4f1 100644 --- a/apps/web/src/components/sidebar.tsx +++ b/apps/web/src/components/sidebar.tsx @@ -27,7 +27,6 @@ import { SelectContent, SelectItem, } from "@methanium/ui"; -import { isTauri } from "@tauri-apps/api/core"; import { useIsMobile } from "@methanium/ui"; import { MobileNavbar } from "./navbar"; @@ -35,24 +34,20 @@ import SidebarBox from "@tensamin/call/sidebarBox"; import { useShowMobileNavbar } from "@/routes/app/useShowMobileNavbar"; import { Ellipsis, Check } from "lucide-react"; import { useState } from "react"; -import type { User } from "@tensamin/user/context"; -import type z from "zod"; -import { mtp } from "@tensamin/shared/data"; +import { useUser, type User } from "@tensamin/user/context"; +import { mtp, userPresencePreferenceSchema } from "@tensamin/shared/data"; import { useMTP } from "@tensamin/mtp"; +import { + onlineStatusLabels, + onlineStatusOptions, + type OnlineStatus, +} from "./status-options"; -type OnlineStatus = z.infer; - -const onlineStatusLabels: Record = { - user_online: "Online", - user_offline: "Offline", - user_dnd: "Do not disturb", - user_idle: "Idle", - user_wc: "Away", - user_borked: "Borked", - iota_offline: "Iota offline", - iota_online: "Iota online", - iota_borked: "Iota borked", -}; +function accountPreference(status: User["OnlineStatus"]): OnlineStatus { + return userPresencePreferenceSchema.safeParse(status).success + ? (status as OnlineStatus) + : "user_online"; +} function StatusDialog({ user, @@ -68,7 +63,7 @@ function StatusDialog({ saveSucceeded, setSaveSucceeded, }: { - user: User; + user: Pick; open: boolean; onOpenChange: (open: boolean) => void; send: ReturnType["send"]; @@ -81,13 +76,15 @@ function StatusDialog({ saveSucceeded: boolean; setSaveSucceeded: (value: boolean) => void; }) { + const { updateProfile, updateState } = useUser(); + return ( { if (!nextOpen) { setDraftStatus(user.Status ?? ""); - setDraftOnlineStatus(user.OnlineStatus); + setDraftOnlineStatus(accountPreference(user.OnlineStatus)); setErrorMessage(""); setSaveSucceeded(false); } @@ -126,10 +123,11 @@ function StatusDialog({ - Online - Offline - Idle - Do not disturb + {onlineStatusOptions.map((option) => ( + + {option.label} + + ))}
@@ -140,12 +138,10 @@ function StatusDialog({ Cancel} /> - )} - /> - - Hide users - - - )} -
+
+ {!isMobile && ( +
+ {view === "focused" && ( + + ( + + )} + /> + + Hide users + + + )} +
+ )} - -
- - ( - - )} - /> - - Popout - - - - ( - - )} - /> - - Fullscreen - - -
+ {!isMobile && ( +
+ + ( + + )} + /> + + Popout + + + + ( + + )} + /> + + Fullscreen + + +
+ )}
); } diff --git a/packages/call/src/components/buttons/invite.tsx b/packages/call/src/components/buttons/invite.tsx index b478529..d49a643 100644 --- a/packages/call/src/components/buttons/invite.tsx +++ b/packages/call/src/components/buttons/invite.tsx @@ -63,6 +63,7 @@ export default function InviteButton({ Loading...
} component={(user) => ( + ); + + return ( + <> + + + + tooltip ? ( + ( + } + onClick={onClick} + className={className} + > + {trigger} + + )} + /> + ) : ( + + {trigger} + + ) + } + /> + + + + {isScreensharing ? ( + + ) : null} + {cameraEnabled ? ( + + ) : null} + + + {tooltip ? ( + + {tooltip} + + ) : null} + + + {dialogKind ? ( + !open && setDialogKind(null)} + portalContainer={portalContainer} + /> + ) : null} + + ); +} diff --git a/packages/call/src/components/buttons/screenshare.tsx b/packages/call/src/components/buttons/screenshare.tsx deleted file mode 100644 index 4312dda..0000000 --- a/packages/call/src/components/buttons/screenshare.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { useEffect, useState } from "react"; -import { - Button, - Popover, - PopoverContent, - PopoverTrigger, - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@methanium/ui"; -import { MonitorDot, ScreenShare } from "lucide-react"; -import { toast } from "@tensamin/shared/log"; -import { isTauri } from "@tauri-apps/api/core"; -import { setScreenShareEnabled, useCall } from "../../store"; -import ScreenShareDialog from "../screenshareDialog"; - -export default function ScreenshareButton({ - className, - iconSize, - tooltip, - defaultPortal, -}: { - className?: string; - iconSize?: number; - tooltip?: string; - defaultPortal?: boolean; -}) { - const isScreensharing = useCall((state) => state.screenShareEnabled); - const screenRef = useCall((state) => state.screenRef); - const [portalContainer, setPortalContainer] = useState(); - const [dialogOpen, setDialogOpen] = useState(false); - const [menuOpen, setMenuOpen] = useState(false); - - useEffect(() => { - if (defaultPortal) return; - setPortalContainer(screenRef?.current ?? undefined); - }, [screenRef, defaultPortal]); - - async function startWebShare() { - try { - await setScreenShareEnabled(true, { - audio: true, - systemAudio: "include", - surfaceSwitching: "include", - video: true, - }); - } catch (error) { - console.error("Failed to start web screen share", error); - toast( - "error", - error instanceof Error - ? error.message - : "Failed to start screen sharing.", - ); - } - } - - async function stopShare() { - try { - await setScreenShareEnabled(false); - } catch (error) { - console.error("Failed to stop screen share", error); - toast("error", "Failed to stop screen sharing."); - } - } - - return ( - <> - - - - tooltip ? ( - ( - - )} - /> - ) : ( - - ) - } - /> - - - - - - - {tooltip && ( - - {tooltip} - - )} - - - {isTauri() && ( - - )} - - ); -} diff --git a/packages/call/src/components/invitePopup.tsx b/packages/call/src/components/invitePopup.tsx index ed26a1a..583c57d 100644 --- a/packages/call/src/components/invitePopup.tsx +++ b/packages/call/src/components/invitePopup.tsx @@ -23,6 +23,7 @@ export default function InvitePopup({ return ( ( diff --git a/packages/call/src/components/mediaShareDialog.tsx b/packages/call/src/components/mediaShareDialog.tsx new file mode 100644 index 0000000..8e7b493 --- /dev/null +++ b/packages/call/src/components/mediaShareDialog.tsx @@ -0,0 +1,223 @@ +import { useEffect, useRef, useState } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Label, + Switch, +} from "@methanium/ui"; +import { toast } from "@tensamin/shared/log"; +import { Loader2 } from "lucide-react"; +import { + getMediaShareAdapter, + type MediaShareKind, + type MediaShareSource, +} from "../mediaShare"; +import { startCameraShare, startScreenShare } from "../store"; +import MediaSourceCard from "./mediaSourceCard"; + +export default function MediaShareDialog({ + kind, + open, + onOpenChange, + portalContainer, +}: { + kind: MediaShareKind; + open: boolean; + onOpenChange: (open: boolean) => void; + portalContainer?: HTMLElement; +}) { + const [loading, setLoading] = useState(false); + const [sources, setSources] = useState([]); + const [selectedSourceId, setSelectedSourceId] = useState(null); + const [shareAudio, setShareAudio] = useState(true); + const [canShareAudio, setCanShareAudio] = useState(false); + const [cameraPreview, setCameraPreview] = useState(null); + const [cameraPreviewVersion, setCameraPreviewVersion] = useState(0); + const cameraPreviewRef = useRef(null); + + function stopCameraPreview() { + cameraPreviewRef.current?.getTracks().forEach((track) => track.stop()); + cameraPreviewRef.current = null; + setCameraPreview(null); + } + + useEffect(() => { + if (!open) return; + let active = true; + setLoading(true); + setSelectedSourceId(null); + + Promise.all([ + getMediaShareAdapter().listSources(kind), + getMediaShareAdapter().getCapabilities(), + ]) + .then(([nextSources, capabilities]) => { + if (!active) return; + const availableSources = + kind === "camera" && nextSources.length === 0 + ? [ + { + id: "__default_camera__", + kind: "camera" as const, + name: "Default camera", + }, + ] + : nextSources; + setSources(availableSources); + setSelectedSourceId(availableSources[0]?.id ?? null); + setCanShareAudio(kind === "screen" && capabilities.canShareScreenAudio); + }) + .catch((error) => { + console.error("Failed to load media share sources", error); + toast("error", "Failed to load media sources."); + }) + .finally(() => active && setLoading(false)); + + return () => { + active = false; + }; + }, [kind, open]); + + useEffect(() => { + if (kind !== "camera" || !open || !selectedSourceId) { + stopCameraPreview(); + return; + } + + let active = true; + stopCameraPreview(); + const sourceId = + selectedSourceId === "__default_camera__" ? undefined : selectedSourceId; + + void navigator.mediaDevices + .getUserMedia({ + audio: false, + video: sourceId ? { deviceId: { exact: sourceId } } : true, + }) + .then((stream) => { + if (!active) { + stream.getTracks().forEach((track) => track.stop()); + return; + } + cameraPreviewRef.current = stream; + setCameraPreview(stream); + }) + .catch((error) => { + console.error("Failed to preview camera", error); + }); + + return () => { + active = false; + stopCameraPreview(); + }; + }, [cameraPreviewVersion, kind, open, selectedSourceId]); + + async function startSharing() { + setLoading(true); + try { + if (kind === "camera") { + stopCameraPreview(); + await startCameraShare( + selectedSourceId === "__default_camera__" + ? undefined + : (selectedSourceId ?? undefined), + ); + } else { + await startScreenShare({ + sourceId: selectedSourceId ?? undefined, + includeAudio: canShareAudio && shareAudio, + }); + } + onOpenChange(false); + } catch (error) { + console.error(`Failed to share ${kind}`, error); + if (kind === "camera") { + setCameraPreviewVersion((version) => version + 1); + } + toast( + "error", + error instanceof Error ? error.message : `Failed to share ${kind}.`, + ); + } finally { + setLoading(false); + } + } + + return ( + + + + + {kind === "camera" ? "Share a camera" : "Share your screen"} + + + +
+
+ {sources.map((source) => { + const selected = source.id === selectedSourceId; + return ( + setSelectedSourceId(source.id)} + /> + ); + })} +
+ + {!loading && sources.length === 0 ? ( +

+ No {kind === "camera" ? "cameras" : "windows or displays"} found. +

+ ) : null} + + {canShareAudio ? ( +
+
+ +

+ Some apps and protected media do not allow audio capture. +

+
+ +
+ ) : null} + + {loading ? ( +
+ + Loading sources... +
+ ) : null} +
+ + + + + +
+
+ ); +} diff --git a/packages/call/src/components/mediaSourceCard.tsx b/packages/call/src/components/mediaSourceCard.tsx new file mode 100644 index 0000000..f740dbc --- /dev/null +++ b/packages/call/src/components/mediaSourceCard.tsx @@ -0,0 +1,77 @@ +import { cn } from "@methanium/ui"; +import { AppWindow, Camera, MonitorUp } from "lucide-react"; +import { useEffect, useRef } from "react"; +import type { MediaShareSource } from "../mediaShare"; + +function SourceIcon({ source }: { source: MediaShareSource }) { + const className = "size-8 text-muted-foreground"; + + if (source.kind === "camera") return ; + if (source.kind === "window") return ; + return ; +} + +function VideoPreview({ stream }: { stream: MediaStream }) { + const videoRef = useRef(null); + + useEffect(() => { + const video = videoRef.current; + if (!video) return; + video.srcObject = stream; + void video.play().catch(() => undefined); + + return () => { + video.srcObject = null; + }; + }, [stream]); + + return ( +