release: 0.2.0 — Android app (development preview) + new app icon - #102
Merged
Merged
Conversation
Introduces MobileSyncScreen (Screen 09): heading, P2P QR export card (reuses the desktop P2P start-server + base64-key JSON format), E2E Supabase status card, and "Scan QR code" button. Settings "Supabase sync" and "Pair new device" rows now push MobileSyncScreen. Scan header reskinned to MobileColors/mobileHeading.
Reject empty URL, non-http/https URL, and empty anon key in _saveConfig instead of writing broken state to SyncProvider. Shows an inline error (keyed sync-config-error) and keeps the form open. Sync code remains optional (unchanged). 4 new widget tests cover the three rejection paths and the valid round-trip (including setSyncCode exercised with a Crockford-safe code).
Fix 8 lint issues in mobile test files: - unnecessary_underscores in 3 files (use named params instead of _/__) - unused_local_variable noSession in snippets test (use it as provider value) - prefer_function_declarations_over_variables in probe test (clock fn) Close tracked minor gaps: - Replace raw SizedBox(height:4) with MobileTokens.space1 in snippets screen - Add probeAll debounce unit test (concurrent duplicate ids not double-probed) - Add widget test asserting _EntryRow shows Icons.lock_outline for .env file Regression confirmed: AppLockGate wraps app in mobile_app.dart, TofuWatcher in mobile_home_shell.dart, pinch-zoom + CursorDrag in terminal screen, P2P QR import reachable via mobile_qr_scan_screen / mobile_sync_screen, no dangling refs to deleted mobile_sessions_screen.dart.
…assphrase, remove dead-end controls
Finding 1: add KeyGenService to MobileBootstrap providers list — opening
the Generate sheet no longer throws ProviderNotFoundException at runtime.
Finding 2: capture addKeyFromFile return value in _GenerateSheetState._generate
and call keyProv.savePassphrase when passphrase is non-empty — mirrors the
Import flow; generated passphrase-protected keys are now usable after restart.
Finding 3: remove all no-op / misleading interactive controls —
- port_forward_screen: replace empty PopupMenuButton with SizedBox
- sync_screen: remove placeholder more_vert IconButton from AppBar
- terminal_screen: remove dead Split IconButton (mobile has no split)
- hosts_screen: replace no-op _CircleButton header items with non-interactive
_DecorativeCircle; remove now-dead _CircleButton class
- settings_screen: remove no-op tune AppBar action; render Auto-lock row as
display-only (drop onTap/chevron since the feature is not built)
Finding 4: add MobileColors.fieldFill (0xFF1C1C1E) and MobileColors.tabInactive
(0xFF6D6D72) tokens; replace all hardcoded occurrences in mobile_hosts_screen,
mobile_keys_screen, and mobile_tab_bar with the tokens.
Tests: extend mobile_bootstrap_test to assert KeyGenService resolves from the
provider tree; add two unit tests in keys_screen_test asserting savePassphrase
is called iff passphrase is non-empty.
…, safe to delete later)
The launcher artwork was a thin-stroke terminal window exported by hand to one bitmap per platform. Below ~32 px the strokes blurred into an amber blob, and with no shared source the macOS iconset, Windows .ico, Android bitmaps and in-app logos drifted apart. Replace all of it with vector masters in assets/branding/ plus a generator: - Composite artwork: amber squircle (superellipse corners, so it matches the macOS/iOS shape) with a solid `>_` prompt glyph in #141416, weighted to survive a 16 px raster. - tool/gen_app_icons.py renders every target straight from the vectors via sips, so no output is an upscale of a smaller PNG, and re-running it is idempotent. It verifies each PNG's real dimensions before accepting it. - Android gets proper adaptive layers: the foreground glyph is scaled to 0.95 to stay inside the 66dp-of-108dp safe circle under any OEM mask, and a monochrome layer enables Android 13+ themed icons. The background is a gradient drawable rather than five PNG buckets — sharp at any density and ~380 KB smaller in the APK — with its colours read out of the master SVG so there is still one source of truth. values/colors.xml went away with the bitmap background it existed for; the manifest now also declares roundIcon. - macOS rasters are inset to 824/1024 per Apple's icon grid so the glyph matches the optical size of system icons in the Dock. Windows ships a 16–256 px multi-size ICO. Verified: APK resource compile (icon payload 511 KB -> 131 KB), the macOS .appiconset compiling to AppIcon.icns, and all four Android mask shapes plus the themed layer rendered down to 36 px. Refs #95
`flutter build macos` failed on this branch with "'PasskeysPlugin' is only available in macOS 13.5 or newer". Adding the mobile dependencies (mobile_scanner, local_auth, google_fonts) re-resolved the lockfile and carried supabase_flutter from 2.12.4 up to 2.15.0; release 2.13 added a passkeys dependency whose passkeys_darwin plugin is annotated @available(macOS 13.5), while the Runner targets MACOSX_DEPLOYMENT_TARGET 10.15 — so the generated macOS plugin registrant stopped compiling. Android was unaffected, which is why the break went unnoticed. Resolve the lockfile against develop instead of raising the deployment target, which would drop macOS 10.15-13.4 for every desktop user. The SDK itself is genuinely used (supabase_service, share_session_service), so it cannot be dropped either. pubspec.yaml records why the version is held. Regenerated plugin registrants follow: passkeys leaves the macOS and Windows registrants, and device_info_plus / ua_client_hints go with it — they were only there as supabase_flutter 2.15.0 transitives. macOS Podfile.lock now carries the two mobile pods that do support it (local_auth_darwin, mobile_scanner). Verified: flutter build macos, flutter build apk, flutter analyze clean, 1720 tests green. Fixes #96
The workflow only triggered on pull_request into master, but master is a release-only branch — every feature PR targets develop. So feature PRs reported no checks at all (`gh pr checks` says "no checks reported") and the suite first ran when a release PR reached master, long after the work had been reviewed and merged into develop. Add develop to the trigger list so analyze, the app tests and both Rust crates' tests gate the branch where work actually lands.
…e old icon The generator config was still in pubspec.yaml pointing at the previous artwork — `adaptive_icon_background: "#000000"` and a 388 KB `assets/android_adaptive_fg.png`. Anyone running `dart run flutter_launcher_icons` would have silently overwritten the new adaptive layers with the old black-background icon, and the dev dependency invited exactly that. It also could not have produced the current icons anyway: it covers Android/iOS only, takes a raster input, and has no notion of the per-platform insets the macOS icon grid needs. Remove the config, the dev dependency and the orphaned foreground asset; pubspec.yaml now points at tool/gen_app_icons.py instead. Also stroke yourssh_mark.svg with currentColor: its ink #141416 scores 1.00:1 against the desktop sidebar #141414 and 1.02:1 against the mobile surface, so a bare mark dropped onto app chrome would have been invisible. The consumer picks the colour now.
Two follow-ups on the develop trigger: - actions/checkout@v4 runs on Node 20, which the runners now force onto Node 24 with a deprecation annotation. v7 is current (v5 moved to Node 24, v6 writes credentials to a separate file, v7 blocks fork checkouts for pull_request_target / workflow_run and moves to ESM) — none of which affects a plain checkout on a pull_request event. - Add a concurrency group keyed on the PR so pushing again cancels the run still in flight instead of compiling the IronRDP and VNC trees twice side by side. A full run is ~9 minutes, most of it Rust.
Tapping a host on Android connected fine but never navigated: the card sat in its connecting state for the whole session and the terminal only appeared once the shell had closed. _openSession awaited SessionProvider.connectAny before pushing the terminal route, and that future does not resolve when the connection is ready — _doConnect awaits SshService.openShell, which completes only when the shell closes. Start the connection and navigate straight away instead: the session is registered synchronously, so the terminal can pick it up and render its own connecting state, which is exactly what the desktop call sites do with unawaited(connectAny(...)). The existing nav test passed because its fake connectAny returned immediately. Added a fake that behaves like the real one — session registered synchronously, future left pending — which fails against the old code and passes now. Fixes #98
The main manifest declared only CAMERA. Flutter injects INTERNET into the debug and profile manifests for its own tooling, so every development build had network access and nothing looked wrong — but a release APK/AAB would have installed with no network permission at all, failing every SSH connection, SFTP transfer and port forward on the shipped app. Fixes #99
Captures all 14 mobile screens off a real device: hosts list, per-host action sheet, new-host form, snippets, keys, settings, sync/QR pairing, and — against a live SSH server — the connecting state, a terminal running real commands, the accessory key bar, the ⋮ menu, the SFTP browser and port forwarding. Runs under `flutter drive` rather than `flutter test`: the latter uninstalls the app at the end of the run, taking anything written inside the app's directories with it, so frames come back over the driver connection and test_driver/integration_test.dart writes them on the host. The Android surface is converted with convertFlutterSurfaceToImage first, since takeScreenshot cannot read a SurfaceView. The terminal and SFTP shots need a real server: a linuxserver/openssh-server container on 2222, which the emulator reaches as 10.0.2.2:2222. The test seeds demo hosts / keys / snippets, puts the demo password in secure storage, disables the biometric lock (no enrolled fingerprint on an emulator), and restores every touched pref in a finally block. Without the container those shots degrade to the failure state instead of failing the run. Screenshots land in screenshots/11-mobile/. The skill doc records the invocation plus the emulator gotchas (full /data means INSTALL_FAILED_INSUFFICIENT_STORAGE for a ~185 MB debug APK).
The RDP and VNC Dart tests started failing with Bad state: yourssh_rdp's codegen version (2.12.0) should be the same as runtime version (2.13.0) as soon as flutter_rust_bridge 2.13.0 hit pub.dev. rust/Cargo.toml already pins the crate exactly (=2.12.0) and lib/src/generated/ was produced by codegen 2.12.0, but the Dart dependency allowed a range, so a fresh resolve — which is what CI does, since package lockfiles are not committed — picked up a runtime the generated code refuses to work with. Pin the Dart side exactly, so all three move together. Bumping the bridge now means regenerating lib/src/generated/ and updating Cargo.toml in the same change, which is how flutter_rust_bridge expects to be upgraded. Fixes #100
feat(mobile): Android mobile app (v1)
One sequential job took ~9m45s, and the two heaviest steps do not depend on each other at all: the yourssh_rdp cargo tests (165s) and the app suite (176s). Nothing under app/test loads the native bridge — verified by hiding assets/native and running the RDP/VNC model and workspace tests, which still pass — so the app work never needed to queue behind Rust. Split into an `app` job and a `bridge` matrix job per crate. Each crate keeps its cargo tests, build.sh and Dart tests together: both cargo invocations use --release, so the library build reuses the test build's compilation, and splitting them would compile the IronRDP / vnc-rs tree twice. fail-fast is off so a broken RDP crate does not hide a broken VNC crate, and rust-cache is keyed per crate workspace. Wall clock becomes the longest job (~5 min, the RDP crate) instead of the sum. It costs two extra Flutter/toolchain setups in compute — cheaper waiting, more machine time. master's branch protection requires a status check literally named "test", so the split keeps a job with that name: an aggregate gate that needs both others and fails unless both succeeded. It runs with `always()` — otherwise a failure upstream would skip it and leave the required check pending forever rather than red.
ci: split the suite into parallel jobs
Minor bump rather than the usual patch: Android is a new target, not another feature on the existing ones. - Version to 0.2.0 in app/pubspec.yaml; CHANGELOG's Unreleased block folded into the release; roadmap and wiki version headers updated; the roadmap's "in progress" Android entry and the README's unchecked Android box now read as shipped, both stating the debug-signing caveat. - README: Android in the platform badge and a row in the download table. - release.yml gains a build-android job. No Rust step — the mobile UI is SSH-only, so nothing pulls the RDP/VNC native libraries into the Android build. The APK is signed with the debug key that build.gradle.kts falls back to when key.properties is absent, which is called out in the workflow, the CHANGELOG, the README and the roadmap: it installs and runs, but it cannot go to Play and a later properly-signed build cannot upgrade it in place. Verified locally before wiring the job: `flutter build apk --release` succeeds (85 MB) and the release APK really does carry android.permission.INTERNET, which only landed in the main manifest this cycle.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ships everything on
developsince 0.1.40: the Android target as a development preview, and a redrawn app icon across every platform. Minor bump rather than the usual patch because Android is a new target, not another feature on the existing ones.Changes
0.2.0inapp/pubspec.yaml; CHANGELOG's Unreleased block folded into the release; roadmap + wiki version headers updated.build.gradle.ktsfalls back to whenkey.propertiesis absent. It installs and runs, but it cannot go to Play, and a later properly-signed build cannot upgrade it in place. A real release keystore has to land before that changes.release.ymlgains abuild-androidjob — no Rust step, since the mobile UI is SSH-only and nothing pulls the RDP/VNC native libraries into the Android build. EmitsYourSSH-0.2.0-Android.apkplus its debug symbols, and thereleasejob now waits on it and attaches the APK.What is in 0.2.0
Added — Android app (development preview): hosts list with search / folder chips / tag grouping and live reachability probes; terminal with the accessory key bar (sticky Ctrl/Alt, ^C/^D, arrows, special characters), pinch-zoom and long-press cursor drag; single-panel SFTP; snippets quick-run; key generation and import; port-forwarding rules; host import over cloud Supabase pull or P2P QR camera scan; biometric app-lock and the TOFU dialog. RDP/VNC, plugins, recording and the audit log stay desktop-only.
Changed — new app icon: vector masters in
assets/branding/, rendered per platform bytool/gen_app_icons.py(macOS iconset inset to Apple's 824/1024 grid, 16–256 px Windows ICO, Android adaptive layers incl. an Android 13+ monochrome layer, in-app/README logos). Glyph holds 6.4–12:1 contrast on the amber plate; icon reads at 6.3–7.3:1 against every dark app surface.Also fixed on the way in (all Android/CI-side, none of it ever shipped to users): the Android terminal never opening on tap (#98), the release manifest missing
INTERNET(#99), the macOS build broken by asupabase_flutterlockfile drift intopasskeys(#96), and CI breaking wheneverflutter_rust_bridgepublishes (#100).Type of change
release— version release to `master`How was this tested?
Before wiring the new job: `flutter build apk --release` locally (85 MB) and `aapt2 dump permissions` on the resulting APK confirming `android.permission.INTERNET` is present in a release build — that permission only reached the main manifest this cycle, so it was worth checking rather than assuming.
On `develop`: `flutter analyze` clean, 1721 tests green, `flutter build macos` green, and the parallel CI suite (`app`, `bridge (yourssh_rdp)`, `bridge (yourssh_vnc)`, `test`) green at 5.9 min.
Mobile screens captured on an Android 16 emulator against a real SSH server are in `screenshots/11-mobile/`.
Note for the merge
Merging this to `master` triggers `release.yml`, which builds and publishes tag `v0.2.0` with the macOS DMG/ZIP, Windows x64+arm64, Linux amd64+arm64, and now the Android APK.