M12 — the Android client, end to end #2

Merged
bvandeusen merged 86 commits from dev into main 2026-08-21 08:53:58 -04:00
75 changed files with 246 additions and 1346 deletions
Showing only changes of commit 0a7480cf9b - Show all commits
-97
View File
@@ -1,97 +0,0 @@
# Android (Tauri v2 mobile) — SEPARATE from desktop.yml because it is a different
# surface with a different toolchain, not a different bundle of the same one. The
# Rust core is cross-compiled with the NDK and then packaged by the Gradle project
# in desktop/src-tauri/gen/android.
#
# Toolchain comes from the ci-tauri-android image (Rust + the Android ABIs +
# SDK/NDK + JDK 17 + tauri-cli); runs-on is only a scheduling label (Label Model B).
#
# Android, desktop and web are peer surfaces on one quality bar, so this triggers on
# the same shared frontend that desktop.yml does — the frontend is compiled INTO the
# app by generate_context!, so a frontend commit that skipped this lane would ship a
# stale phone build.
name: Android (Tauri)
on:
push:
branches: [dev, main]
tags: ["v*"]
paths:
- "desktop/**"
- "frontend/**"
- ".forgejo/workflows/android.yml"
workflow_dispatch:
concurrency:
group: android-${{ github.ref }}
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}
permissions:
contents: read
jobs:
build:
name: Android APK (debug)
runs-on: python-ci
container:
image: git.fabledsword.com/bvandeusen/ci-tauri-android:1.97
steps:
- uses: actions/checkout@v6
# Same reason as the desktop lanes: generate_context! embeds the built
# frontend at compile time, so it must exist before cargo runs.
- name: Build the shared frontend
run: npm ci && npm run build
working-directory: frontend
# The lockfile gate, matching the desktop lanes (issue 2102). Fetching for the
# Android target also pre-warms exactly the crates the build will want.
- name: Verify the lockfile and fetch dependencies
run: cargo fetch --locked --target aarch64-linux-android
working-directory: desktop/src-tauri
# DEBUG, and arm64 only, deliberately.
#
# Debug because a release APK has to be signed, and the release keystore must
# be generated by the operator and never pass through CI logs or an agent
# session (the constraint recorded on task 2136 for the updater key applies
# identically here). Gradle's throwaway debug keystore needs nothing from
# anyone, so this lane can prove the app COMPILES and PACKAGES today and grow
# a signed release job when a keystore exists.
#
# arm64 + x86_64. arm64 is every real device; x86_64 is what a desktop
# emulator runs, and an arm64-only APK installs there and then dies unable to
# load its native library — so a build nobody can try on an emulator is a
# build nobody checks. armv7 and i686 are left out: 32-bit hardware we do not
# target, and the image carries all four if that changes.
- name: Tauri build (Android APK)
run: |
version="$(sh ../packaging/build-version.sh)"
echo "Building version $version"
cargo tauri android build \
--debug \
--target aarch64 \
--target x86_64 \
--config '{"build":{"beforeBuildCommand":""}}' \
--config "{\"version\":\"$version\"}"
working-directory: desktop/src-tauri
# Located rather than hardcoded: the output path depends on the target and
# build type, and a wrong literal here would fail as "no files found" long
# after the expensive step succeeded.
- name: Locate the APK
run: |
apk="$(find desktop/src-tauri/gen/android/app/build/outputs/apk -name '*.apk' -type f | head -1)"
[ -n "$apk" ] || { echo "ERROR: no APK produced" >&2; exit 1; }
echo "Built $apk ($(du -h "$apk" | cut -f1))"
echo "APK_PATH=$apk" >> "$GITHUB_ENV"
# Mirrored action, never actions/upload-artifact — see desktop.yml's Upload
# bundles step for the full reasoning. Pinned by SHA because the mirror
# auto-syncs.
- name: Upload the APK
uses: https://git.fabledsword.com/bvandeusen/upload-artifact@cb8afe72b42edc798abfb8fcb556cf660d894245
with:
name: thoughtsync-android-debug
path: ${{ env.APK_PATH }}
if-no-files-found: error
+11 -5
View File
@@ -20,6 +20,13 @@ on:
tags: ["v*"]
paths:
- "desktop/**"
# The shared client core (store + sync engine) the desktop wraps. Its own
# crate since the Android client binds the same code, so a change there is a
# change to this app even though nothing under desktop/ moved.
- "core/**"
# The workspace manifest and lockfile, which now live at the repo root.
- "Cargo.toml"
- "Cargo.lock"
# The whole frontend, not just the adapter/bridge seam: it is compiled INTO
# the desktop binary, so any part of it changing means the shipped app is out
# of date. Config and lockfile included — a dependency bump changes the bundle
@@ -142,7 +149,6 @@ jobs:
cargo tauri signer sign "$appimage"
[ -s "$appimage.sig" ] || { echo "ERROR: re-signing produced no .sig" >&2; exit 1; }
echo "Re-signed $(basename "$appimage")"
working-directory: desktop/src-tauri
# install.sh hands the .deb to every Debian/Ubuntu user, so the package's
# Depends must be right BEFORE a release exists. Prints the generated
@@ -180,9 +186,9 @@ jobs:
with:
name: thoughtsync-linux
path: |
desktop/src-tauri/target/release/bundle/appimage/*.AppImage
desktop/src-tauri/target/release/bundle/deb/*.deb
desktop/src-tauri/target/release/bundle/arch/*.pkg.tar.*
target/release/bundle/appimage/*.AppImage
target/release/bundle/deb/*.deb
target/release/bundle/arch/*.pkg.tar.*
# error, not warn: a build that bundles nothing should report as a
# failure, not as a green run with an empty artifact.
if-no-files-found: error
@@ -296,7 +302,7 @@ jobs:
uses: https://git.fabledsword.com/bvandeusen/upload-artifact@cb8afe72b42edc798abfb8fcb556cf660d894245
with:
name: thoughtsync-windows
path: desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
path: target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
if-no-files-found: error
# Publishes to the SAME release as the Linux job. Safe to run twice: the
+3
View File
@@ -174,3 +174,6 @@ cython_debug/
# PyPI configuration file
.pypirc
# Rust workspace build output (one target dir for core + desktop + android)
/target/
+14 -4
View File
@@ -3946,7 +3946,7 @@ dependencies = [
]
[[package]]
name = "thoughtsync-desktop"
name = "thoughtsync-core"
version = "0.1.0"
dependencies = [
"chrono",
@@ -3957,11 +3957,21 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"uuid",
]
[[package]]
name = "thoughtsync-desktop"
version = "0.1.0"
dependencies = [
"log",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-log",
"tauri-plugin-updater",
"uuid",
"thoughtsync-core",
]
[[package]]
@@ -5294,9 +5304,9 @@ dependencies = [
[[package]]
name = "zerovec-derive"
version = "0.11.4"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62"
checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9"
dependencies = [
"proc-macro2",
"quote",
+26
View File
@@ -0,0 +1,26 @@
# Rust workspace. Two members today: the framework-free client core, and the Tauri
# desktop app that wraps it. The Android client becomes a third consumer of `core`
# through uniffi (Scribe note 2730) — which is the reason the core is a crate at all
# rather than a module inside the desktop app.
[workspace]
resolver = "2"
members = ["core", "desktop/src-tauri"]
# Shared pins, so two consumers of the core cannot drift onto different versions of
# the same dependency and resolve differently.
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
log = "0.4"
# Tauri's default release profile: smaller, faster shipped binaries.
#
# At the WORKSPACE root, not in the desktop member: cargo ignores profiles declared
# by a non-root package, so leaving it there would silently drop lto/strip/opt-level
# from every release build with only a warning to say so.
[profile.release]
codegen-units = 1
lto = true
opt-level = "s"
panic = "abort"
strip = true
+11 -46
View File
@@ -159,49 +159,14 @@ backend/frontend push.
- No Postgres lane (unchanged): the desktop app's local store + sync behavior is
verified on the operator's machine, not in CI.
## Android (Tauri mobile) lane — `android.yml`, third image
## Android lane — REMOVED 2026-08-17
Android is a peer surface, not a desktop variant, so it gets its own workflow.
- **Image:** `git.fabledsword.com/bvandeusen/ci-tauri-android:1.97` (Rust + the
four Android ABIs + Android SDK/NDK + JDK 17 + Node + tauri-cli). Pins held in
lockstep with `ci-tauri` and `ci-tauri-win` — all three compile the same crate.
- **Why a third image.** `ci-android` says in its own header that it excludes the
NDK ("No Flutter, no NDK, no CMake") and carries no Rust; `ci-tauri` has Rust
but no SDK, plus a WebKitGTK stack Android never touches. Tauri needs the NDK
precisely because the core is Rust compiled to Android ABIs.
- **JDK 17, not the 25 `ci-android`/`ci-flutter` ship.** `tauri android init`
GENERATES the Gradle project, so its version isn't ours to choose — it produced
Gradle **8.14.3** + AGP **8.11.0** here. `CI-android/versions.env` records that
JDK 25 needs Gradle 9.1.0+ and that anything older fails with an opaque
`"25.0.3"` message. Bumping this JDK without checking what Tauri templates will
break the build with an error that names nothing useful.
- **`gen/android/` is COMMITTED.** `tauri android init` generates it, and the only
ignores are its own (build outputs, `local.properties`, `key.properties`,
`keystore.properties`). CI must not have to regenerate it, and manifest or
Gradle edits have to survive.
- **Debug APK only, arm64 only, today.** A release APK must be signed, and the
keystore has to be generated by the operator and never pass through CI logs or
an agent session — the same constraint recorded for the updater key (task 2136).
Gradle's throwaway debug keystore needs nothing from anyone, so this lane proves
the app compiles and packages. arm64 is every real device; the image carries the
other three ABIs, so widening is a one-word change.
- **Green means it BUILT.** Like the Windows lane, a Linux runner cannot execute
the artifact. Nothing here proves the app runs, renders, or is usable by finger.
- **TLS: OpenSSL is vendored on Android.** `reqwest` is pinned to `native-tls`,
which is deliberate for the Windows lane (it resolves to `schannel`, keeping
C/asm out of the cross-compile). On Android it resolves to **OpenSSL**, and the
first build duly failed with `openssl-sys`: *"Could not find directory of
OpenSSL installation"* — there is no Android OpenSSL to link against.
`Cargo.toml` now carries a `cfg(target_os = "android")` block enabling
`openssl-sys`'s `vendored` feature, which compiles OpenSSL from source with the
NDK toolchain. That is why the image ships `perl` + `make`.
- **Why not `rustls` on Android.** It builds faster and was the obvious fix, but
rustls ships its own root store — the phone would trust a DIFFERENT set of
certificates than the desktop. A self-hosted server behind a private or
enterprise CA would then work on one surface and fail on another. One TLS stack
across all surfaces is worth more than the build minutes. Revisit only if
vendored OpenSSL becomes the thing that breaks this lane repeatedly.
The Tauri-mobile Android lane is gone. Android is a native Kotlin/Compose client
over the shared `thoughtsync-core` crate instead — see Scribe note 2730 for the
decision and milestone M12 for the arc. Its CI lane and image will be rebuilt on
that basis; `ci-tauri-android` is being repurposed (drop `tauri-cli`, add
`cargo-ndk`) rather than deleted, because its NDK binutils symlinks and PATH fix
are load-bearing for any Rust-on-Android cross-compile.
## Formatting the Rust lane before pushing
@@ -212,8 +177,8 @@ a formatter is neither a test run nor a local stack:
```
docker run --rm --user "$(id -u):$(id -g)" -e CARGO_HOME=/tmp/cargo \
-v "$PWD/desktop/src-tauri:/w" -w /w \
git.fabledsword.com/bvandeusen/ci-tauri:1.97 cargo fmt --check
-v "$PWD:/w" -w /w \
git.fabledsword.com/bvandeusen/ci-tauri:1.97 cargo fmt --all --check
```
Drop `--check` to apply. `--user` keeps the container from leaving root-owned
@@ -225,7 +190,7 @@ literal — copying that shape caused one of the four failures.
## The desktop lockfile
`desktop/src-tauri/Cargo.lock` is **committed**, per Cargo's own guidance for
`Cargo.lock` is **committed** at the workspace root, per Cargo's own guidance for
binary crates. Without it every CI run re-resolved the graph, which meant a
released `.deb`/`.AppImage`/`.exe` couldn't be rebuilt from its tag, a build
could break with no repo change, and Renovate had nothing to bump (issue 2102).
@@ -244,7 +209,7 @@ above, and resolution is neither a test run nor a build:
```
docker run --rm --user "$(id -u):$(id -g)" -e CARGO_HOME=/tmp/cargo \
-v "$PWD/desktop/src-tauri:/w" -w /w \
-v "$PWD:/w" -w /w \
git.fabledsword.com/bvandeusen/ci-tauri:1.97 cargo generate-lockfile
```
+45
View File
@@ -0,0 +1,45 @@
[package]
name = "thoughtsync-core"
version = "0.1.0"
description = "ThoughtSync client core — local-first SQLite store and opt-in sync engine"
authors = ["bvandeusen"]
edition = "2021"
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
# Local-first store (M10.4): bundled = compile SQLite in, so there's no system
# libsqlite dependency to vary across the AppImage / native / Windows / Android builds.
rusqlite = { version = "0.32", features = ["bundled"] }
uuid = { version = "1", features = ["v4"] }
# RFC3339 timestamps for created_at/updated_at/remind_at (Date.parse-able on the JS side).
chrono = { version = "0.4", default-features = false, features = ["clock"] }
# HTTP for the opt-in server handshake (M10.6) and the sync engine (M10.7).
#
# native-tls, NOT rustls, deliberately: on x86_64-pc-windows-msvc native-tls resolves
# to `schannel` — pure-Rust bindings to the OS TLS stack — so nothing C or assembly
# has to cross-compile on the Windows lane, which is the fragile one. rustls would
# instead pull in ring/aws-lc-rs and their assembler. On Linux native-tls uses
# OpenSSL, whose headers (libssl-dev) ci-tauri already ships.
# default-features off drops http2/charset we don't need for a JSON API.
reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }
# Verifying downloaded attachment bytes against the sha256 the server advertised.
sha2 = "0.10"
# Android has no system OpenSSL to link against, and `native-tls` resolves to
# OpenSSL there — unlike Windows, where it lands on schannel and costs nothing.
# Without this the build dies at `openssl-sys`: "Could not find directory of
# OpenSSL installation".
#
# `vendored` compiles OpenSSL from source with the NDK toolchain. The alternative
# was rustls on Android only, which builds faster — but rustls ships its own root
# store, so the phone would trust a DIFFERENT set of certificates than the desktop
# does. A self-hosted server behind a private or enterprise CA would then work on
# one surface and fail on another, and "the surfaces behave the same" is worth more
# than build minutes.
#
# Declared as a direct dependency purely to turn the feature on: cargo's feature
# unification applies it to the copy `native-tls` pulls in transitively.
[target.'cfg(target_os = "android")'.dependencies]
openssl-sys = { version = "0.9", features = ["vendored"] }
+14
View File
@@ -0,0 +1,14 @@
//! ThoughtSync's client core: the on-device SQLite store and the sync engine.
//!
//! Deliberately free of any UI framework. The desktop wraps it in Tauri commands;
//! the Android client binds it through uniffi. Neither owns it, and a change to
//! either must not require touching this crate — that separation is the whole point
//! (see Scribe note 2730). It was already true before the split: every file here
//! carried zero Tauri references, which is what made the extraction a move rather
//! than a rewrite.
//!
//! - `local` — the source of truth. Works with no server and no account.
//! - `sync` — entirely opt-in. Nothing in it runs until a server is linked.
pub mod local;
pub mod sync;
@@ -1,8 +1,9 @@
//! The local-first core: an on-device SQLite store + Tauri commands implementing the
//! frontend's repository seam, so the desktop app is fully usable with no server and
//! no login. adapters/local.ts (M10.5) calls into `commands`.
//! The local-first store: on-device SQLite, and the source of truth for every
//! client. A client built on this is fully usable with no server and no account.
//!
//! Framework-free on purpose. The desktop reaches it through Tauri commands and
//! Android through uniffi, but neither of those concerns appears in here.
pub mod commands;
pub mod derive;
pub mod models;
pub mod retention;
@@ -14,9 +15,10 @@ use std::sync::Mutex;
use rusqlite::Connection;
/// The shared database handle, held in Tauri's managed state. rusqlite connections
/// aren't `Sync`, so a `Mutex` serializes access (fine operations are quick and the
/// UI is single-user).
/// The shared database handle. rusqlite connections aren't `Sync`, so a `Mutex`
/// serializes access fine, since operations are quick and a client is single-user.
/// How it is held is the caller's business: Tauri manages it as state, Android holds
/// it in the uniffi object.
pub struct Db(pub Mutex<Connection>);
/// Open (creating if needed) the database at `path` and bring it to the latest schema.
@@ -7,14 +7,13 @@
//! be talked to at all. Pure decision logic, no I/O.
//! - `client` — HTTP transport: the handshake call and device-token auth.
//! - `state` — the persisted link record (server, token, change-feed cursor).
//! - `commands` — the Tauri surface the Settings UI drives.
//! - `engine` — one full cycle: push local changes, then pull the server's.
//!
//! The engine that moves notes — push, pull, last-write-wins — lands in M10.7b/c and
//! consults `compat` before it does anything.
//! The UI surface that drives this lives in whichever client is wrapping the crate,
//! not here.
pub mod blobs;
pub mod client;
pub mod commands;
pub mod compat;
pub mod engine;
pub mod pull;
@@ -20,7 +20,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
BUNDLE_DIR="$REPO_ROOT/desktop/src-tauri/target/release/bundle/appimage"
BUNDLE_DIR="$REPO_ROOT/target/release/bundle/appimage"
# appimagetool is published only under the rolling "continuous" tag (the project
# cuts no semver releases), so this URL is the pinned distribution channel.
@@ -90,7 +90,7 @@ fi
echo "==> Locating appimagetool"
# Prefer the copy Tauri already downloaded during the build (no network, and
# version-matched to the toolchain that produced the AppImage).
APPIMAGETOOL="$(find "$REPO_ROOT/desktop/src-tauri/target" -name 'appimagetool-*.AppImage' -type f 2>/dev/null | head -n1 || true)"
APPIMAGETOOL="$(find "$REPO_ROOT/target" -name 'appimagetool-*.AppImage' -type f 2>/dev/null | head -n1 || true)"
if [ -z "${APPIMAGETOOL:-}" ]; then
echo " not cached by Tauri; downloading from continuous channel"
APPIMAGETOOL="$WORK/appimagetool"
+2 -2
View File
@@ -29,8 +29,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
SRC_TAURI="$REPO_ROOT/desktop/src-tauri"
BINARY="$SRC_TAURI/target/release/thoughtsync"
OUT_DIR="${1:-$SRC_TAURI/target/release/bundle/arch}"
BINARY="$REPO_ROOT/target/release/thoughtsync"
OUT_DIR="${1:-$REPO_ROOT/target/release/bundle/arch}"
PKGNAME="thoughtsync"
# The name this package used to ship under. pacman needs both to retire it: without
+1 -1
View File
@@ -33,7 +33,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
DEB_DIR="$REPO_ROOT/desktop/src-tauri/target/release/bundle/deb"
DEB_DIR="$REPO_ROOT/target/release/bundle/deb"
shopt -s nullglob
DEBS=("$DEB_DIR"/*.deb)
+6 -6
View File
@@ -14,10 +14,10 @@
# for a tag that already exists.
#
# The build + de-bundle steps run first; this consumes their output:
# desktop/src-tauri/target/release/bundle/appimage/*.AppImage (de-bundled)
# desktop/src-tauri/target/release/bundle/deb/*.deb
# desktop/src-tauri/target/release/bundle/arch/*.pkg.tar.* (prebuilt pacman)
# desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
# target/release/bundle/appimage/*.AppImage (de-bundled)
# target/release/bundle/deb/*.deb
# target/release/bundle/arch/*.pkg.tar.* (prebuilt pacman)
# target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
#
# Instance-agnostic: server + repo come from the runner's github.* context
# (Forgejo populates them for compatibility), so nothing is hardcoded to one host.
@@ -44,9 +44,9 @@ AUTH=(-H "Authorization: token $GITHUB_TOKEN")
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
BUNDLE_ROOT="$REPO_ROOT/desktop/src-tauri/target/release/bundle"
BUNDLE_ROOT="$REPO_ROOT/target/release/bundle"
# Cross-compiled Windows output lands under the target triple, not the host root.
WIN_BUNDLE_ROOT="$REPO_ROOT/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle"
WIN_BUNDLE_ROOT="$REPO_ROOT/target/x86_64-pc-windows-msvc/release/bundle"
# --- collect the assets to upload -------------------------------------------
shopt -s nullglob
-3
View File
@@ -1,5 +1,2 @@
# Generated by Cargo
/target/
# Generated by Tauri
/gen/schemas
+18 -53
View File
@@ -9,11 +9,15 @@ edition = "2021"
# src/main.rs.
autobins = false
# App logic lives in the library (Tauri v2 pattern: a single `run()` entry point
# reusable across desktop and any future mobile target); main.rs is a thin shim.
# App logic lives in the library (Tauri v2 pattern: a single `run()` entry point);
# main.rs is a thin shim.
#
# rlib only. The staticlib/cdylib types existed for Tauri mobile, which Android no
# longer uses — it is a native Kotlin client over `thoughtsync-core` instead
# (Scribe note 2730), and the .so it loads is built from that crate, not this one.
[lib]
name = "thoughtsync_desktop_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
crate-type = ["rlib"]
# The shipped command is `thoughtsync` on every install channel, and the binary's
# own name is what the desktop actually keys on: GTK derives the window's WM_CLASS
@@ -32,58 +36,19 @@ path = "src/main.rs"
tauri-build = { version = "2", features = [] }
[dependencies]
# The framework-free client core: local SQLite store + sync engine. Shared with the
# Android client, which binds the same crate through uniffi (Scribe note 2730).
thoughtsync-core = { path = "../../core" }
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Local-first store (M10.4): bundled = compile SQLite in, so there's no system
# libsqlite dependency to vary across the AppImage / native / Windows builds.
rusqlite = { version = "0.32", features = ["bundled"] }
uuid = { version = "1", features = ["v4"] }
# RFC3339 timestamps for created_at/updated_at/remind_at (Date.parse-able on the JS side).
chrono = { version = "0.4", default-features = false, features = ["clock"] }
serde = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
# Startup + operation logging to stdout AND a persistent file, so portability
# issues are diagnosable from any environment. `log` is the facade the code uses.
# issues are diagnosable from any environment.
tauri-plugin-log = "2"
log = "0.4"
# In-app updates (M10.9). Signature verification is minisign; the public half lives
# in tauri.conf.json and the private half only ever as a CI secret.
# in tauri.conf.json and the private half only ever as a CI secret. Desktop only —
# the plugin declares android support level "none", which is why the Android client
# gets a server-served update path instead (Scribe note 2725).
tauri-plugin-updater = "2"
# HTTP for the opt-in server handshake (M10.6) and, next, the sync engine (M10.7).
#
# native-tls, NOT rustls, deliberately: on x86_64-pc-windows-msvc native-tls
# resolves to `schannel` — pure-Rust bindings to the OS TLS stack — so nothing C or
# assembly has to cross-compile on the Windows lane, which is the fragile one (it
# builds on Linux via cargo-xwin, and a C dependency there is what broke it before).
# rustls would instead pull in ring/aws-lc-rs and their assembler. On Linux
# native-tls uses OpenSSL, whose headers (libssl-dev) ci-tauri already ships.
# default-features off drops http2/charset we don't need for a JSON API.
reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }
# Verifying downloaded attachment bytes against the sha256 the server advertised.
# Pure Rust (no C/asm beyond optional cpufeatures), so it costs the Windows
# cross-compile lane nothing — see ci-requirements.md on why that matters here.
sha2 = "0.10"
# Android has no system OpenSSL to link against, and `native-tls` resolves to
# OpenSSL there — unlike Windows, where it lands on schannel and costs nothing.
# Without this the build dies at `openssl-sys`: "Could not find directory of
# OpenSSL installation".
#
# `vendored` compiles OpenSSL from source with the NDK toolchain (hence perl + make
# in ci-tauri-android). The alternative was rustls on Android only, which builds
# faster — but rustls ships its own root store, so the phone would trust a
# DIFFERENT set of certificates than the desktop does. A self-hosted server behind
# a private or enterprise CA would then work on one surface and fail on another,
# and "the surfaces behave the same" is worth more here than build minutes.
#
# Declared as a direct dependency purely to turn the feature on: cargo's feature
# unification applies it to the copy `native-tls` pulls in transitively.
[target.'cfg(target_os = "android")'.dependencies]
openssl-sys = { version = "0.9", features = ["vendored"] }
# Tauri's default release profile: smaller, faster shipped binaries.
[profile.release]
codegen-units = 1
lto = true
opt-level = "s"
panic = "abort"
strip = true
@@ -1,12 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
-20
View File
@@ -1,20 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build
/captures
.externalNativeBuild
.cxx
local.properties
key.properties
keystore.properties
/.tauri
/tauri.settings.gradle
@@ -1,6 +0,0 @@
/src/main/**/generated
/src/main/jniLibs/**/*.so
/src/main/assets/tauri.conf.json
/tauri.build.gradle.kts
/proguard-tauri.pro
/tauri.properties
@@ -1,71 +0,0 @@
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rust")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
android {
compileSdk = 36
namespace = "com.fabledsword.thoughtsync"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "com.fabledsword.thoughtsync"
minSdk = 24
targetSdk = 36
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
isMinifyEnabled = true
proguardFiles(
*fileTree(".") { include("**/*.pro") }
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
.toList().toTypedArray()
)
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
}
rust {
rootDirRel = "../../../"
}
dependencies {
implementation("androidx.webkit:webkit:1.14.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.lifecycle:lifecycle-process:2.10.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
apply(from = "tauri.build.gradle.kts")
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- AndroidTV support -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.thoughtsync_desktop"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- AndroidTV support -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
@@ -1,11 +0,0 @@
package com.fabledsword.thoughtsync
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}
}
@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

@@ -1,6 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.thoughtsync_desktop" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -1,4 +0,0 @@
<resources>
<string name="app_name">"ThoughtSync"</string>
<string name="main_activity_title">"ThoughtSync"</string>
</resources>
@@ -1,6 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.thoughtsync_desktop" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
@@ -1,22 +0,0 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.11.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register("clean").configure {
delete("build")
}
@@ -1,23 +0,0 @@
plugins {
`kotlin-dsl`
}
gradlePlugin {
plugins {
create("pluginsForCoolKids") {
id = "rust"
implementationClass = "RustPlugin"
}
}
}
repositories {
google()
mavenCentral()
}
dependencies {
compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:8.11.0")
}
@@ -1,68 +0,0 @@
import java.io.File
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
open class BuildTask : DefaultTask() {
@Input
var rootDirRel: String? = null
@Input
var target: String? = null
@Input
var release: Boolean? = null
@TaskAction
fun assemble() {
val executable = """cargo""";
try {
runTauriCli(executable)
} catch (e: Exception) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
// Try different Windows-specific extensions
val fallbacks = listOf(
"$executable.exe",
"$executable.cmd",
"$executable.bat",
)
var lastException: Exception = e
for (fallback in fallbacks) {
try {
runTauriCli(fallback)
return
} catch (fallbackException: Exception) {
lastException = fallbackException
}
}
throw lastException
} else {
throw e;
}
}
}
fun runTauriCli(executable: String) {
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
val target = target ?: throw GradleException("target cannot be null")
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("tauri", "android", "android-studio-script");
project.exec {
workingDir(File(project.projectDir, rootDirRel))
executable(executable)
args(args)
if (project.logger.isEnabled(LogLevel.DEBUG)) {
args("-vv")
} else if (project.logger.isEnabled(LogLevel.INFO)) {
args("-v")
}
if (release) {
args("--release")
}
args(listOf("--target", target))
}.assertNormalExitValue()
}
}
@@ -1,85 +0,0 @@
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.get
const val TASK_GROUP = "rust"
open class Config {
lateinit var rootDirRel: String
}
open class RustPlugin : Plugin<Project> {
private lateinit var config: Config
override fun apply(project: Project) = with(project) {
config = extensions.create("rust", Config::class.java)
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
extensions.configure<ApplicationExtension> {
@Suppress("UnstableApiUsage")
flavorDimensions.add("abi")
productFlavors {
create("universal") {
dimension = "abi"
ndk {
abiFilters += abiList
}
}
defaultArchList.forEachIndexed { index, arch ->
create(arch) {
dimension = "abi"
ndk {
abiFilters.add(defaultAbiList[index])
}
}
}
}
}
afterEvaluate {
for (profile in listOf("debug", "release")) {
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
val buildTask = tasks.maybeCreate(
"rustBuildUniversal$profileCapitalized",
DefaultTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for all targets"
}
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
for (targetPair in targetsList.withIndex()) {
val targetName = targetPair.value
val targetArch = archList[targetPair.index]
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
val targetBuildTask = project.tasks.maybeCreate(
"rustBuild$targetArchCapitalized$profileCapitalized",
BuildTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for $targetArch"
rootDirRel = config.rootDirRel
target = targetName
release = profile == "release"
}
buildTask.dependsOn(targetBuildTask)
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
targetBuildTask
)
}
}
}
}
}
@@ -1,24 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonFinalResIds=false
Binary file not shown.
@@ -1,6 +0,0 @@
#Tue May 10 19:22:52 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
-185
View File
@@ -1,185 +0,0 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
-89
View File
@@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -1,3 +0,0 @@
include ':app'
apply from: 'tauri.settings.gradle'
@@ -6,11 +6,11 @@
use serde_json::Value;
use tauri::State;
use crate::local::models::*;
use crate::local::retention;
use crate::local::store;
use crate::local::Db;
use crate::sync::state;
use thoughtsync_core::local::models::*;
use thoughtsync_core::local::retention;
use thoughtsync_core::local::store;
use thoughtsync_core::local::Db;
use thoughtsync_core::sync::state;
// A macro would hide the (very regular) locking; kept explicit so each command reads
// as an obvious lock -> delegate -> stringify.
+10
View File
@@ -0,0 +1,10 @@
//! The Tauri command surface — the desktop's adapter onto `thoughtsync-core`.
//!
//! This is the whole of what couples the store and sync engine to Tauri, and keeping
//! it in one place is deliberate: the Android client writes its own adapter (uniffi)
//! against the same core, so anything that leaks framework concerns back into the
//! core makes that second adapter harder. `frontend/src/adapters/local.ts` calls in
//! here over `invoke`.
pub mod local;
pub mod sync;
@@ -6,13 +6,13 @@
use serde::{Deserialize, Serialize};
use tauri::State;
use crate::local::Db;
use crate::sync::blobs::BlobStore;
use crate::sync::client::{self, Identity, ProbeResult};
use crate::sync::compat::Compatibility;
use crate::sync::engine;
use crate::sync::push;
use crate::sync::state;
use thoughtsync_core::local::Db;
use thoughtsync_core::sync::blobs::BlobStore;
use thoughtsync_core::sync::client::{self, Identity, ProbeResult};
use thoughtsync_core::sync::compat::Compatibility;
use thoughtsync_core::sync::engine;
use thoughtsync_core::sync::push;
use thoughtsync_core::sync::state;
/// Ask a server who it is, without committing to anything. The UI calls this as the
/// user finishes typing an address, so they see what answered before handing over
+57 -53
View File
@@ -1,20 +1,24 @@
//! ThoughtSync desktop (Tauri v2).
//!
//! The window loads the shared Vue 3 frontend (`../../frontend`). The Rust core will
//! own the on-device SQLite store (M10.4) and the opt-in sync engine (M10.7), which
//! the frontend reaches through the `frontend/src/adapters/` seam (M10.3) over Tauri
//! `invoke`. Today it exposes desktop integration (menu-entry install for the Linux
//! AppImage) and boots the UI.
//! The window loads the shared Vue 3 frontend (`../../frontend`), which reaches the
//! store and sync engine through the `frontend/src/adapters/` seam (M10.3) over Tauri
//! `invoke`.
//!
//! This crate is the DESKTOP WRAPPER, not the core. The on-device SQLite store and
//! the sync engine live in `thoughtsync-core`, shared with the Android client; what
//! remains here is the Tauri command surface (`commands`), desktop integration
//! (menu-entry install for the Linux AppImage), the in-app updater, and boot.
mod commands;
mod integration;
mod local;
// `pub` (unlike the modules above) because parts of it have no in-crate caller yet —
// the engine that will consume them is M10.7b/c, and a private module's unreachable
// items read as dead code.
pub mod sync;
mod update;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
// The store and the sync engine live in the shared `thoughtsync-core` crate, which
// the Android client binds through uniffi (Scribe note 2730). Aliased to their old
// names so every call site below reads exactly as it did when they were modules of
// this crate — the extraction changed where they live, not what they are.
use thoughtsync_core::{local, sync};
pub fn run() {
use tauri_plugin_log::{Target, TargetKind};
@@ -93,48 +97,48 @@ pub fn run() {
integration::integration_status,
integration::integrate_desktop,
integration::unintegrate_desktop,
local::commands::config_get,
local::commands::auth_me,
local::commands::notes_list,
local::commands::notes_get,
local::commands::notes_create,
local::commands::notes_create_titled,
local::commands::notes_update,
local::commands::notes_complete_reminder,
local::commands::notes_snooze_reminder,
local::commands::notes_set_labels,
local::commands::notes_add_item,
local::commands::notes_update_item,
local::commands::notes_delete_item,
local::commands::notes_delete_attachment,
local::commands::notes_delete_preview,
local::commands::notes_reorder,
local::commands::notes_trash,
local::commands::notes_restore,
local::commands::notes_delete_forever,
local::commands::notes_revisions,
local::commands::notes_restore_revision,
local::commands::notes_reminders,
local::commands::notes_titles,
local::commands::notes_search,
local::commands::notes_backlinks,
local::commands::notes_link_search,
local::commands::labels_list,
local::commands::labels_create,
local::commands::labels_rename,
local::commands::labels_set_color,
local::commands::labels_remove,
local::commands::labels_merge,
local::commands::saved_filters_list,
local::commands::saved_filters_create,
local::commands::saved_filters_remove,
local::commands::saved_filters_rename,
sync::commands::sync_probe,
sync::commands::sync_link,
sync::commands::sync_unlink,
sync::commands::sync_status,
sync::commands::sync_now,
sync::commands::sync_has_pending,
commands::local::config_get,
commands::local::auth_me,
commands::local::notes_list,
commands::local::notes_get,
commands::local::notes_create,
commands::local::notes_create_titled,
commands::local::notes_update,
commands::local::notes_complete_reminder,
commands::local::notes_snooze_reminder,
commands::local::notes_set_labels,
commands::local::notes_add_item,
commands::local::notes_update_item,
commands::local::notes_delete_item,
commands::local::notes_delete_attachment,
commands::local::notes_delete_preview,
commands::local::notes_reorder,
commands::local::notes_trash,
commands::local::notes_restore,
commands::local::notes_delete_forever,
commands::local::notes_revisions,
commands::local::notes_restore_revision,
commands::local::notes_reminders,
commands::local::notes_titles,
commands::local::notes_search,
commands::local::notes_backlinks,
commands::local::notes_link_search,
commands::local::labels_list,
commands::local::labels_create,
commands::local::labels_rename,
commands::local::labels_set_color,
commands::local::labels_remove,
commands::local::labels_merge,
commands::local::saved_filters_list,
commands::local::saved_filters_create,
commands::local::saved_filters_remove,
commands::local::saved_filters_rename,
commands::sync::sync_probe,
commands::sync::sync_link,
commands::sync::sync_unlink,
commands::sync::sync_status,
commands::sync::sync_now,
commands::sync::sync_has_pending,
update::update_channel_get,
update::update_channel_set,
update::update_check,
+2 -2
View File
@@ -21,7 +21,7 @@ use serde::{Deserialize, Serialize};
use tauri::State;
use tauri_plugin_updater::UpdaterExt;
use crate::local::{store, Db};
use thoughtsync_core::local::{store, Db};
/// Where the manifests live. Fixed tags, so these URLs are permanent.
const FEED_BASE: &str = "https://git.fabledsword.com/bvandeusen/thoughtsync/releases/download";
@@ -308,9 +308,9 @@ fn describe_check_error(detail: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::local::schema;
use rusqlite::Connection;
use std::sync::Mutex;
use thoughtsync_core::local::schema;
fn db() -> Db {
let conn = Connection::open_in_memory().expect("in-memory db");
-117
View File
@@ -1,117 +0,0 @@
# Running the Android client locally
The Android app is the same Vue frontend and Rust core as the desktop, built by
Tauri v2 mobile. This sheet is the local setup; CI's copy of it is the
`ci-tauri-android` image (see `ci-requirements.md`).
**Open this directory in Android Studio:**
```
desktop/src-tauri/gen/android
```
Two things must be true first, and both fail confusingly if they aren't.
## 1. Gradle must run on JDK 17
Android Studio defaults to its own bundled JDK, which is currently 25. That
produces:
> Incompatible Gradle JVM version — The project's Gradle version 8.14.3 is
> incompatible with the Gradle JVM version 25 currently selected to run Gradle
> build.
Fix: `Settings → Build, Execution, Deployment → Build Tools → Gradle → Gradle
JDK`**17**. If none is listed, `Download JDK…` → version 17, vendor Eclipse
Temurin.
Prefer 17 explicitly over Android Studio's "apply a compatible JDK" quick-fix,
which will likely pick 21. 17 is what `ci-tauri-android` pins, and matching it
means a local build and a CI build resolve identically.
**Why 8.14.3 and not Gradle 9:** `cargo tauri android init` generated the wrapper
pin, and Gradle 9 removed `project.exec`, which the generated
`buildSrc/.../BuildTask.kt` still uses. The wrapper, the AGP pin and that file are
all tracked in this repo, so the bump is ours to make whenever it is worth doing —
it is scaffolding we inherited, not a requirement of Tauri. Tauri's own Android
layer targets `compileSdk 36` and registers back handling through
`OnBackPressedDispatcher`, so the library is current; only the template trails.
## 2. The Gradle glue has to be generated once
`settings.gradle` does `apply from: 'tauri.settings.gradle'`, and that file — with
`app/tauri.build.gradle.kts`, `app/tauri.properties`,
`app/src/main/assets/tauri.conf.json` and the compiled `.so` files — is generated
per build and gitignored. A fresh clone does not have them, and settings
evaluation happens before any Gradle task, so the sync fails before anything can
generate them.
From `desktop/src-tauri`:
```sh
cargo tauri android build --debug --target x86_64
```
After that, Android Studio syncs cleanly.
## Prerequisites
Gradle shells out to `cargo tauri android android-studio-script` on every build,
so the Rust toolchain is required on the machine running Android Studio — the CI
image cannot stand in for it.
```sh
rustup toolchain install 1.97.1
rustup target add aarch64-linux-android x86_64-linux-android
cargo install tauri-cli --version 2.11.4 --locked
```
Via the SDK Manager, matching `CI-tauri-android/versions.env`:
| | version | why it matters |
|---|---|---|
| Platform | API 36 | `compileSdk` / `targetSdk` |
| Build-Tools | 35.0.0 | AGP 8.11's default |
| **NDK** | 28.2.13676358 | the Rust core cannot cross-compile without it |
| Platform-Tools | latest | `adb` |
```sh
export ANDROID_HOME=$HOME/Android/Sdk
export NDK_HOME=$ANDROID_HOME/ndk/28.2.13676358
```
Node is needed too — `beforeBuildCommand` runs `npm ci && npm run build`, because
`generate_context!` compiles the frontend into the binary.
## Emulator
Create an AVD with an **x86_64** system image, API 24 or newer (`minSdk = 24`).
CI builds arm64-v8a and x86_64, so either runs, but x86_64 executes natively on a
desktop instead of being translated.
## The dev loop
Android Studio's Run button rebuilds the whole Rust core each time. For
iteration, with an emulator already running:
```sh
cd desktop/src-tauri
cargo tauri android dev
```
That starts Vite, wires `adb reverse` so the emulator reaches
`localhost:5173`, and hot-reloads frontend changes without a rebuild. Android
Studio earns its keep for the debugger, logcat and layout inspection.
## Known gaps, so they are not mistaken for bugs
- **Safe areas / viewport** are not handled yet (task 2706) — content may sit
under a notch or the gesture bar. Deliberately left until a device could
verify it.
- **`android:enableOnBackInvokedCallback`** is absent from the manifest, so
predictive-back animations will not engage on Android 13+. Tauri registers the
dispatcher correctly; only the opt-in flag is missing.
- **`android:usesCleartextTraffic`** is templated in app-wide. A
`networkSecurityConfig` resource is the better shape — see Scribe note 2437,
which hit this on Minstrel.
- Debug builds are large and slow to start. Do not read either as the product.