build(android): cfg-gate desktop-only Tauri surfaces for android target
Phase 1 of the Android same-repo target plan: make the workspace
compilable for `aarch64-linux-android` (and the other NDK ABIs) by
removing the desktop-only crate dependencies and command bodies from the
Android build. After this commit, `tauri android init` followed by
`cargo tauri android build` is structurally unblocked — the remaining
work is the SDK/NDK toolchain (off-sandbox), the Svelte single-window
refactor, and the Phase 3 MVP feature surface.
What's gated under `cfg(not(target_os = "android"))`:
- src-tauri/Cargo.toml: `tauri = { features = ["tray-icon"] }` is now
declared in the desktop-only target block. The `global-shortcut`,
`window-state`, and `autostart` plugins join it — none of the three
support Android natively. The base `tauri = "2"` plus `dialog`,
`opener`, and `notification` plugins remain unconditional because they
do support Android.
- src-tauri/src/lib.rs: `mod tray` declaration, the matching
`tray::setup(app)` call, the close-to-tray `WindowEvent::CloseRequested`
handler, and the `.plugin(tauri_plugin_global_shortcut::*)` /
`_autostart` / `_window_state` chain are all desktop-only. The
builder is split with a single `#[cfg(not(target_os = "android"))]`
branch that adds the desktop plugins on top of the universal base.
- src-tauri/src/commands/tts.rs: `tts_speak` previously had three
`#[cfg(target_os = ...)]` branches but no fallback, so on Android the
`spawned` binding was unbound and the function failed to compile.
Mirrored the existing `paste.rs` not-implemented fallback. Same fix
for `list_voices_impl`. Frontend will hide the Read Page Aloud button
on Android via `isAndroid()`.
- src-tauri/src/commands/windows.rs: all four multi-window commands
(`open_task_window`, `open_preview_window`, `close_preview_window`,
`open_viewer_window`) get an Android stub that returns a clear
"Multi-window is not supported on Android" error. Tauri on Android
is single-Activity; the previously-secondary content (preview overlay,
transcript viewer, task float) will live as routes inside the main
window, gated by `isAndroid()` on the frontend.
What's *not* changed:
- Top-level `identifier` in tauri.conf.json stays `uk.co.corbel.kon`.
The Phase 10b Kon → Corbie rename sweep will land
`corbel.technology.corbie` as part of a coherent rebrand commit
rather than fragmenting the rename across this branch.
- `bundle.android.minSdkVersion: 24` added so a future
`tauri android init` knows to target Android 7.0+ (Vulkan available,
scoped storage starts at 29 — we'll surface scoped-storage paths
via Tauri's dialog plugin on Phase 3).
- `kon-hotkey` already exports a non-Linux stub; no changes needed.
- `commands/meeting.rs` still calls `process_watch::list_running_process_names()`
which compiles on Android but returns an empty list (SELinux blocks
/proc walk on API 24+). Frontend will hide the toggle on Android.
Verification: 91/91 tests still pass on the buildable-in-sandbox crates
(kon-storage 60, kon-core 16, kon-mcp 9, kon-hotkey 4, kon-cloud-providers
2). svelte-check 0/0 across 3957 files. The src-tauri crate itself can't
be compiled in this sandbox (no webkit2gtk); CI's desktop builders will
exercise the desktop branch, and Jake's Android-equipped dev box will
exercise the Android branch via `tauri android init`.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
This commit is contained in:
@@ -37,16 +37,13 @@ kon-cloud-providers = { path = "../crates/cloud-providers" }
|
||||
kon-hotkey = { path = "../crates/hotkey" }
|
||||
kon-llm = { path = "../crates/llm" }
|
||||
|
||||
# Tauri
|
||||
tauri = { version = "2", features = ["tray-icon"] }
|
||||
# Tauri. The `tray-icon` feature, the global-shortcut/window-state/
|
||||
# autostart plugins are desktop-only — gated below under
|
||||
# cfg(not(target_os = "android")). The dialog, opener, and notification
|
||||
# plugins all support Android natively so live in the unconditional list.
|
||||
tauri = { version = "2" }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-window-state = "2"
|
||||
# Phase 5 rituals: register Corbie as a login-time autostart entry.
|
||||
# Handles platform differences (.desktop file on Linux, LaunchAgents plist
|
||||
# on macOS, registry Run key on Windows) behind a single API.
|
||||
tauri-plugin-autostart = "2"
|
||||
# Phase 6 nudges: OS-native notifications via the frontend-owned nudge
|
||||
# bus. Permission-request flow happens on the JS side before the first
|
||||
# deliver_nudge call; Windows only delivers for installed apps.
|
||||
@@ -79,6 +76,25 @@ uuid = { version = "1", features = ["v4"] }
|
||||
# pollute the workspace.
|
||||
tempfile = "3"
|
||||
|
||||
# Desktop-only Tauri features and plugins. Each item below either fails
|
||||
# to compile against the Android NDK or is a structural no-op on a
|
||||
# single-window mobile app:
|
||||
# - tray-icon: no system tray on mobile.
|
||||
# - global-shortcut: mobile has no global hotkey API; the foreground
|
||||
# notification with a record button is the Android replacement.
|
||||
# - window-state: single-window, fullscreen, no per-window persistence.
|
||||
# - autostart: mobile apps don't auto-start in the desktop sense; if
|
||||
# we ever want a "boot completed" handler that's a separate Android
|
||||
# intent receiver, not this plugin.
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
tauri = { version = "2", features = ["tray-icon"] }
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-window-state = "2"
|
||||
# Phase 5 rituals: register Corbie as a login-time autostart entry.
|
||||
# Handles platform differences (.desktop file on Linux, LaunchAgents plist
|
||||
# on macOS, registry Run key on Windows) behind a single API.
|
||||
tauri-plugin-autostart = "2"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
webkit2gtk = "2.0"
|
||||
# Needed for setting the preview overlay's WindowTypeHint to Utility via
|
||||
|
||||
@@ -287,6 +287,16 @@ fn list_voices_impl() -> Result<Vec<TtsVoice>, String> {
|
||||
.collect())
|
||||
}
|
||||
|
||||
// Non-desktop fallback. Android's native TextToSpeech API would need a
|
||||
// JNI bridge — out of scope for v0.1-android. The frontend already hides
|
||||
// the Read Page Aloud button behind a runtime detection; this stub keeps
|
||||
// the command surface consistent so a stray invoke surfaces a clear
|
||||
// error rather than panicking on an unbound `list_voices_impl`.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
|
||||
fn list_voices_impl() -> Result<Vec<TtsVoice>, String> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
// ---------- Tauri commands ----------
|
||||
|
||||
#[tauri::command]
|
||||
@@ -310,13 +320,26 @@ pub fn tts_speak(
|
||||
let spawned = spawn_macos(trimmed, rate, voice.as_deref())?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let spawned = spawn_windows(trimmed, rate, voice.as_deref())?;
|
||||
// No bundled TTS shim for non-desktop targets. Android has its own
|
||||
// TextToSpeech APIs that would have to be reached over JNI; out of
|
||||
// scope for v0.1-android. Mirrors the paste.rs not-implemented
|
||||
// fallback so the command compiles cleanly across all targets and
|
||||
// surfaces a clear error if the frontend tries to invoke it.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
|
||||
{
|
||||
let _ = (state, rate, voice);
|
||||
return Err("TTS not implemented on this platform".into());
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
||||
{
|
||||
if let Some(c) = spawned {
|
||||
if let Ok(mut guard) = state.child.lock() {
|
||||
*guard = Some(c);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -1,8 +1,46 @@
|
||||
// Multi-window support is desktop-only. Android Tauri apps run as a
|
||||
// single Activity, so `WebviewWindowBuilder` is not available there.
|
||||
// All four commands below have an Android stub that returns a clear
|
||||
// error so frontend invokes don't panic — the frontend itself is
|
||||
// expected to detect Android via `isAndroid()` and route the previously-
|
||||
// secondary content (preview overlay, transcript viewer, task float)
|
||||
// into routes inside the main window instead.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use crate::PreferencesScript;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
const ANDROID_MULTIWINDOW_ERR: &str =
|
||||
"Multi-window is not supported on Android; this command is desktop-only";
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[tauri::command]
|
||||
pub async fn open_task_window(_app: tauri::AppHandle) -> Result<(), String> {
|
||||
Err(ANDROID_MULTIWINDOW_ERR.into())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[tauri::command]
|
||||
pub async fn open_preview_window(_app: tauri::AppHandle) -> Result<(), String> {
|
||||
Err(ANDROID_MULTIWINDOW_ERR.into())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[tauri::command]
|
||||
pub async fn close_preview_window(_app: tauri::AppHandle) -> Result<(), String> {
|
||||
Err(ANDROID_MULTIWINDOW_ERR.into())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[tauri::command]
|
||||
pub async fn open_viewer_window(_app: tauri::AppHandle) -> Result<(), String> {
|
||||
Err(ANDROID_MULTIWINDOW_ERR.into())
|
||||
}
|
||||
|
||||
/// Open a floating always-on-top task window.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[tauri::command]
|
||||
pub async fn open_task_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app.get_webview_window("tasks-float") {
|
||||
@@ -46,6 +84,7 @@ pub async fn open_task_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||
/// The preview is passive: it subscribes to the `transcription-result`
|
||||
/// event and the cross-window `preview-*` events that DictationPage fires
|
||||
/// as it moves through the phases of a dictation run.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[tauri::command]
|
||||
pub async fn open_preview_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app.get_webview_window("transcription-preview") {
|
||||
@@ -115,6 +154,7 @@ pub async fn open_preview_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||
|
||||
/// Hide the transcription preview window without destroying it so the next
|
||||
/// open is instant. Returns Ok even when no preview window exists.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[tauri::command]
|
||||
pub async fn close_preview_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app.get_webview_window("transcription-preview") {
|
||||
@@ -124,6 +164,7 @@ pub async fn close_preview_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// Open the transcript viewer window.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[tauri::command]
|
||||
pub async fn open_viewer_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app.get_webview_window("transcript-viewer") {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
mod commands;
|
||||
// System tray uses Tauri's `tray-icon` feature which is desktop-only.
|
||||
// Android has no tray surface — drop the module entirely on that target.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod tray;
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -132,9 +135,23 @@ pub fn run() {
|
||||
// Settings → About can attach them. Local only; nothing transmitted.
|
||||
commands::diagnostics::install_panic_hook();
|
||||
|
||||
tauri::Builder::default()
|
||||
let builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
// Phase 6 nudges: OS-native notifications. The plugin exposes
|
||||
// isPermissionGranted / requestPermission / sendNotification on
|
||||
// the JS side; our deliver_nudge wrapper guards those calls
|
||||
// against the nudge-bus suppression rules and restricts
|
||||
// invocation to the main window via ensure_main_window.
|
||||
.plugin(tauri_plugin_notification::init());
|
||||
|
||||
// Desktop-only plugins. Each is either unsupported on Android
|
||||
// (global-shortcut, autostart) or structurally meaningless on a
|
||||
// single-window mobile app (window-state). Gating here matches the
|
||||
// Cargo.toml `cfg(not(target_os = "android"))` block that drops the
|
||||
// crates entirely on Android targets.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let builder = builder
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
// Phase 5 rituals: autostart. The plugin registers JS-facing
|
||||
// commands (isEnabled / enable / disable) that the Settings
|
||||
@@ -145,18 +162,14 @@ pub fn run() {
|
||||
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
||||
None,
|
||||
))
|
||||
// Phase 6 nudges: OS-native notifications. The plugin exposes
|
||||
// isPermissionGranted / requestPermission / sendNotification on
|
||||
// the JS side; our deliver_nudge wrapper guards those calls
|
||||
// against the nudge-bus suppression rules and restricts
|
||||
// invocation to the main window via ensure_main_window.
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
// Remember size + position of every window across app restarts.
|
||||
// Without this, secondary windows (preview overlay, task float,
|
||||
// transcript viewer) open at whatever spot the compositor picks,
|
||||
// which feels random. State is persisted per-window-label to
|
||||
// app-data/window-state.json.
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build());
|
||||
|
||||
builder
|
||||
.setup(|app| {
|
||||
// Initialise database (blocking in setup — runs once at startup)
|
||||
let db_path = database_path();
|
||||
@@ -256,7 +269,12 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
// Close-to-tray: hide window instead of exiting
|
||||
// Close-to-tray: hide the window instead of exiting so the
|
||||
// tray icon stays as the canonical entry point. Desktop-only;
|
||||
// mobile has no tray and "hide on close" maps to the
|
||||
// platform's own background-app behaviour.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let win = main_window.clone();
|
||||
main_window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
@@ -265,6 +283,7 @@ pub fn run() {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Store init script for secondary windows (float, viewer)
|
||||
app.manage(PreferencesScript(init_script));
|
||||
@@ -287,6 +306,7 @@ pub fn run() {
|
||||
// hint. No-ops on a fully-supported box.
|
||||
crate::commands::models::emit_runtime_warnings(app.handle());
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
if let Err(e) = tray::setup(app) {
|
||||
eprintln!("Failed to setup tray: {e}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user