From d652c5c7c4700f57b3e535a9902a1979753c9efe Mon Sep 17 00:00:00 2001 From: lorenzomicheli Date: Tue, 29 Sep 2026 07:17:09 +0000 Subject: [PATCH 1/2] android: Back the platform clipboard with ClipboardManager AndroidClipboard kept copied text in process memory only, so copy/paste did not reach other apps and paste never saw the system clipboard. With the clipboard feature enabled, read/write now go through packages::clipboard (ClipboardManager via JNI), keeping the in-memory copy as a fallback when the JNI call fails. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/android/platform.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/android/platform.rs b/src/android/platform.rs index 3c0abd4..e9df00f 100644 --- a/src/android/platform.rs +++ b/src/android/platform.rs @@ -64,10 +64,12 @@ use gpui_wgpu::GpuContext; // ── stub: clipboard ─────────────────────────────────────────────────────────── -/// Android clipboard (thin wrapper over `ClipboardManager` via JNI). +/// Android clipboard. /// -/// A real implementation would call into Java via `jni-rs`; for now we use -/// an in-process string store so the rest of the platform compiles. +/// With the `clipboard` feature this goes through the system `ClipboardManager` +/// (`dev.gpui.mobile.GpuiClipboard`), so text copied in GPUI can be pasted in other +/// apps and vice versa. Without the feature, or when the JNI call fails (the host did +/// not package `GpuiClipboard.java`), it falls back to an in-process string store. #[derive(Default)] pub struct AndroidClipboard { contents: Option, @@ -75,10 +77,19 @@ pub struct AndroidClipboard { impl AndroidClipboard { pub fn read(&self) -> Option { + #[cfg(feature = "clipboard")] + match crate::packages::clipboard::get_text() { + Ok(text) => return text, + Err(err) => log::warn!("system clipboard read failed, using local copy: {err}"), + } self.contents.clone() } pub fn write(&mut self, text: String) { + #[cfg(feature = "clipboard")] + if let Err(err) = crate::packages::clipboard::set_text(&text) { + log::warn!("system clipboard write failed, keeping local copy: {err}"); + } self.contents = Some(text); } From aadd68244a1b3cb8b0192754e46067363b37282c Mon Sep 17 00:00:00 2001 From: Floyd Wang Date: Fri, 9 Oct 2026 14:23:39 +0800 Subject: [PATCH 2/2] android: Wait for each surface destroy by request count `surface_destroyed` waited on a single `SURFACE_RELEASED` flag that the render thread set on `SurfaceDestroyed` and cleared on `SurfaceCreated`. The flag can still read "released" from an earlier destroy while a `SurfaceCreated` posted before the new destroy sits unhandled in the queue, so the wait returned before the render thread had processed either command, and the `Surface` could go away just as the render thread attached to it and drew. Number each destroy request instead and wait until the render thread has handled that many. Commands are handled in order, so everything posted before the destroy has been handled too. `on_surface_created` no longer touches the state. Co-authored-by: lorenzomicheli Co-Authored-By: Claude Opus 5.5 --- src/android/host.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/android/host.rs b/src/android/host.rs index a9a50c5..6dae644 100644 --- a/src/android/host.rs +++ b/src/android/host.rs @@ -57,7 +57,7 @@ use std::{ sync::{ - atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering}, + atomic::{AtomicPtr, AtomicU32, AtomicU64, Ordering}, Arc, Mutex, OnceLock, }, time::Duration, @@ -78,8 +78,10 @@ static COMMANDS: Mutex> = Mutex::new(Vec::new()); /// Set once the render thread is running; guards against a second spawn. static STARTED: OnceLock<()> = OnceLock::new(); -/// Cleared by the render thread once it has released the outgoing surface. -static SURFACE_RELEASED: AtomicBool = AtomicBool::new(true); +/// Surface-destroy requests posted by [`surface_destroyed`], and how many of them the +/// render thread has handled. Each caller waits until its own request is handled. +static DESTROYS_REQUESTED: AtomicU64 = AtomicU64::new(0); +static DESTROYS_HANDLED: AtomicU64 = AtomicU64::new(0); /// Scale factor of the current surface (`f32` bits), for the platform-view hit test /// in [`motion_event`], which runs on the Java UI thread and must not touch the window. @@ -341,7 +343,7 @@ fn render_thread(launch: Launch) { app.attached = None; CURRENT_SURFACE.store(std::ptr::null_mut(), Ordering::SeqCst); } - SURFACE_RELEASED.store(true, Ordering::SeqCst); + DESTROYS_HANDLED.fetch_add(1, Ordering::SeqCst); } Command::HostDestroyed { host } => { app.failed.remove(&host); @@ -430,8 +432,6 @@ fn on_surface_created( scale: f32, app: &mut HostApp, ) { - SURFACE_RELEASED.store(false, Ordering::SeqCst); - if app.failed.contains(&host) { log::info!("gpui-main: host {host} already failed; ignoring its surface"); return; @@ -564,17 +564,19 @@ pub fn host_destroyed(host: HostId) { /// framework, otherwise the `Surface` is torn down underneath a thread that may be /// inside `ANativeWindow_lock`. pub fn surface_destroyed(host: HostId) { + // Commands are handled in order, so once the handled count reaches this request's + // number, this destroy has been handled. A single flag set here and cleared by + // `SurfaceCreated` could still read "released" from an earlier destroy while a + // `SurfaceCreated` posted before this one sits unhandled in the queue, letting the + // `Surface` go while the render thread is about to draw into it. Numbering relies + // on every caller being on the Java UI thread, as `SurfaceHolder.Callback` is. + let request = DESTROYS_REQUESTED.fetch_add(1, Ordering::SeqCst) + 1; post(Command::SurfaceDestroyed { host }); - // One global flag serves every host because `SurfaceHolder.Callback` fires on the - // Java UI thread: while this blocks, no other Activity can reach `surface_created` - // to clear it. Waiting per host would mean polling a map from a blocked UI thread - // for no gain. - // // Bounded wait: a stuck render thread must not turn into an ANR. 2 s is far below // the 5 s input-dispatch timeout while being far above a worst-case frame. let deadline = std::time::Instant::now() + Duration::from_secs(2); - while !SURFACE_RELEASED.load(Ordering::SeqCst) { + while DESTROYS_HANDLED.load(Ordering::SeqCst) < request { if std::time::Instant::now() >= deadline { log::error!( "gpui-main: timed out waiting for the render thread to release the surface"