feat(android): follow the activity lifecycle and build an APK (Issue #42)
- The drawing surface is created once the native window exists (Resumed) and released when the app goes to the background (Suspended); before that everything is saved, as Android may stop the app at any time - Android's back button acts like Escape instead of closing the app - Panics are written to logcat; no update check on Android yet - scripts/package-android.sh builds target/ink-notes_<version>_arm64.apk without Gradle (NDK, aapt2, zipalign, apksigner); signing key from ANDROID_KEYSTORE/ANDROID_KEYSTORE_PASS/ANDROID_KEY_ALIAS, otherwise the debug key Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ce3a1f697b
commit
7faf78ddeb
9 files changed
+1662
-353
No files matched your search
+1443
-342
File diff suppressed because it is too large.
Load diff
+43
-5
@@ -165,7 +165,10 @@ struct NeighborPage {
|
||||
}
|
||||
|
||||
pub struct CanvasState {
|
||||
pub surface: wgpu::Surface<'static>,
|
||||
/// Drawing surface of the window; `None` while the app is in the background on Android
|
||||
/// (the native window only exists between `Resumed` and `Suspended`)
|
||||
surface: Option<wgpu::Surface<'static>>,
|
||||
instance: wgpu::Instance,
|
||||
/// Verwendeter Grafikadapter (für erneute Abfrage der Surface-Fähigkeiten bei Größenänderung)
|
||||
adapter: wgpu::Adapter,
|
||||
/// Multisampling der Zeichenfläche (1 = aus) und das zugehörige Zwischenbild
|
||||
@@ -591,7 +594,8 @@ impl CanvasState {
|
||||
let egui_renderer = egui_wgpu::Renderer::new(&device, render_format(&config), None, 1);
|
||||
|
||||
Ok(Self {
|
||||
surface,
|
||||
surface: Some(surface),
|
||||
instance,
|
||||
adapter,
|
||||
msaa_samples,
|
||||
msaa_view,
|
||||
@@ -727,14 +731,42 @@ impl CanvasState {
|
||||
self.update_uniforms();
|
||||
}
|
||||
|
||||
/// Releases the drawing surface: on Android the native window goes away when the app
|
||||
/// is sent to the background (`Event::Suspended`). Drawing is skipped until [`Self::resume`].
|
||||
pub fn suspend(&mut self) {
|
||||
self.surface = None;
|
||||
}
|
||||
|
||||
/// Creates the drawing surface again for the window (`Event::Resumed` on Android).
|
||||
pub fn resume(&mut self, window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if self.surface.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let surface = self.instance.create_surface(window.clone())?;
|
||||
if !surface
|
||||
.get_capabilities(&self.adapter)
|
||||
.formats
|
||||
.contains(&self.config.format)
|
||||
{
|
||||
return Err("surface format changed".into());
|
||||
}
|
||||
self.surface = Some(surface);
|
||||
// Configures the new surface (the window may have changed size meanwhile)
|
||||
self.resize(window.inner_size());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Passt die Surface und Projektionsmatrizen an eine neue Fenstergröße an.
|
||||
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
||||
if new_size.width > 0 && new_size.height > 0 {
|
||||
self.size = new_size;
|
||||
self.config.width = new_size.width;
|
||||
self.config.height = new_size.height;
|
||||
let Some(surface) = &self.surface else {
|
||||
return;
|
||||
};
|
||||
// Fähigkeiten neu abfragen: Ein nicht (mehr) angebotener Modus führt in wgpu zum Absturz
|
||||
let caps = self.surface.get_capabilities(&self.adapter);
|
||||
let caps = surface.get_capabilities(&self.adapter);
|
||||
if !caps.present_modes.contains(&self.config.present_mode) {
|
||||
match choose_present_mode(&caps.present_modes) {
|
||||
Some(mode) => self.config.present_mode = mode,
|
||||
@@ -750,7 +782,7 @@ impl CanvasState {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
surface.configure(&self.device, &self.config);
|
||||
self.msaa_view = create_msaa_view(&self.device, &self.config, self.msaa_samples);
|
||||
self.stencil_view = create_stencil_view(&self.device, &self.config, self.msaa_samples);
|
||||
self.recalculate_viewport();
|
||||
@@ -1386,7 +1418,13 @@ impl CanvasState {
|
||||
.update_texture(&self.device, &self.queue, *id, image_delta);
|
||||
}
|
||||
|
||||
let output = self.surface.get_current_texture()?;
|
||||
let Some(surface) = &self.surface else {
|
||||
for id in &textures_delta.free {
|
||||
self.egui_renderer.free_texture(id);
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
let output = surface.get_current_texture()?;
|
||||
let view = output.texture.create_view(&wgpu::TextureViewDescriptor {
|
||||
format: Some(render_format(&self.config)),
|
||||
..Default::default()
|
||||
|
||||
+3
-3
@@ -38,9 +38,7 @@ pub mod xopp;
|
||||
|
||||
/// Entry point on Android (Issue #42), called by the native activity.
|
||||
///
|
||||
/// Builds with `cargo rustc --lib --crate-type cdylib --target aarch64-linux-android`. Not
|
||||
/// usable yet: the window surface still has to be created on `Resumed` and dropped on
|
||||
/// `Suspended`.
|
||||
/// `scripts/package-android.sh` builds the library and packs it into an APK.
|
||||
#[cfg(target_os = "android")]
|
||||
#[no_mangle]
|
||||
fn android_main(app: winit::platform::android::activity::AndroidApp) {
|
||||
@@ -51,6 +49,8 @@ fn android_main(app: winit::platform::android::activity::AndroidApp) {
|
||||
.with_max_level(log::LevelFilter::Info)
|
||||
.with_tag("ink-notes"),
|
||||
);
|
||||
// Panics would otherwise only go to stderr, which Android discards
|
||||
std::panic::set_hook(Box::new(|info| log::error!("{info}")));
|
||||
if let Some(dir) = app.internal_data_path() {
|
||||
paths::set_app_dir(dir);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,51 @@ pub fn desktop_event_loop() -> Result<winit::event_loop::EventLoop<()>, winit::e
|
||||
builder.build()
|
||||
}
|
||||
|
||||
/// Waits until the window can be drawn to. On Android the native window only exists after
|
||||
/// `Event::Resumed` (Issue #42); on the desktop it is there right away.
|
||||
pub fn wait_until_resumed(
|
||||
#[allow(unused_variables)] event_loop: &mut winit::event_loop::EventLoop<()>,
|
||||
) {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
use winit::event::Event;
|
||||
use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
|
||||
let mut resumed = false;
|
||||
while !resumed {
|
||||
let status = event_loop.pump_events(None, |event, _| {
|
||||
if matches!(event, Event::Resumed) {
|
||||
resumed = true;
|
||||
}
|
||||
});
|
||||
if let PumpStatus::Exit(_) = status {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the event loop until the app quits. On Android the loop was already started by
|
||||
/// [`wait_until_resumed`], so it is driven with `pump_events` (`run` would refuse to start).
|
||||
pub fn run_event_loop<F>(
|
||||
#[allow(unused_mut)] mut event_loop: winit::event_loop::EventLoop<()>,
|
||||
#[allow(unused_mut)] mut handler: F,
|
||||
) -> Result<(), winit::error::EventLoopError>
|
||||
where
|
||||
F: FnMut(winit::event::Event<()>, &winit::event_loop::EventLoopWindowTarget<()>),
|
||||
{
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
|
||||
loop {
|
||||
if let PumpStatus::Exit(_) = event_loop.pump_events(None, &mut handler) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
event_loop.run(handler)
|
||||
}
|
||||
|
||||
/// Gives the window the app id (Wayland: app_id, X11: WM_CLASS), so that dock and task bar
|
||||
/// show the app icon for it.
|
||||
pub fn with_app_id(
|
||||
|
||||
Reference in new issue
Block a user