Fix open on Android (#921)

* fix open

* more

* more

* mroe
This commit is contained in:
Logan McNaughton
2026-05-19 21:56:36 +02:00
committed by GitHub
parent 5850c514f2
commit e852439b6a
11 changed files with 143 additions and 118 deletions
+2 -2
View File
@@ -267,9 +267,9 @@ jobs:
cache: ${{ github.ref_type != 'tag' }}
- name: Build
run: |
cargo install cargo-apk2 cargo-ndk
cargo install cargo-apk cargo-ndk
source <(cargo ndk-env --target ${{ matrix.target }})
cargo apk2 build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} --lib
cargo apk build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} --lib
collect-artifacts:
name: Collect release artifacts
Generated
+37 -83
View File
@@ -735,25 +735,6 @@ dependencies = [
"hybrid-array",
]
[[package]]
name = "block-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7"
dependencies = [
"objc-sys",
]
[[package]]
name = "block2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f"
dependencies = [
"block-sys",
"objc2 0.5.2",
]
[[package]]
name = "block2"
version = "0.5.1"
@@ -2268,12 +2249,12 @@ dependencies = [
"futures",
"jni 0.22.4",
"mimalloc",
"open",
"postcard",
"rand 0.10.1",
"regex",
"reqwest",
"rfd",
"robius-open",
"sdl3-image-sys",
"sdl3-sys",
"sdl3-ttf-sys",
@@ -2894,16 +2875,6 @@ dependencies = [
"cc",
]
[[package]]
name = "icrate"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642"
dependencies = [
"block2 0.4.0",
"objc2 0.5.2",
]
[[package]]
name = "icu_collections"
version = "2.2.0"
@@ -3182,6 +3153,25 @@ version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -4326,6 +4316,17 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "open"
version = "5.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "openssl-probe"
version = "0.2.1"
@@ -4450,6 +4451,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pbkdf2"
version = "0.13.0"
@@ -5163,30 +5170,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "robius-android-env"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "087fcb3061ccc432658a605cb868edd44e0efb08e7a159b486f02804a7616bef"
dependencies = [
"jni 0.21.1",
"ndk-context",
]
[[package]]
name = "robius-open"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243e2abbc8c1ca8ddc283056d4675b67e452fd527c3741c5318642da37840ff3"
dependencies = [
"cfg-if",
"icrate",
"jni 0.21.1",
"objc2 0.5.2",
"robius-android-env",
"windows 0.54.0",
]
[[package]]
name = "rowan"
version = "0.16.1"
@@ -7324,16 +7307,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "windows"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
dependencies = [
"windows-core 0.54.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.3"
@@ -7377,16 +7350,6 @@ dependencies = [
"windows-core 0.62.2",
]
[[package]]
name = "windows-core"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
dependencies = [
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.2"
@@ -7500,15 +7463,6 @@ dependencies = [
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.4"
+2 -14
View File
@@ -6,7 +6,7 @@ rust-version = "1.95.0"
[features]
default = ["gui"]
gui = ["dep:rfd", "dep:slint", "dep:robius-open", "dep:slint-build"]
gui = ["dep:rfd", "dep:slint", "dep:open", "dep:slint-build"]
[lib]
crate-type = ["cdylib", "rlib"]
@@ -19,7 +19,6 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-big-array = "0.5"
sha2 = "0.11"
robius-open = { version = "0.1", default-features = false, optional = true }
sdl3-sys = { version = "0.6", features = ["build-from-source-static"] }
sdl3-ttf-sys = { version = "0.6", features = ["build-static-vendored", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
sdl3-image-sys = { version = "0.6", features = ["build-static-vendored"] }
@@ -43,6 +42,7 @@ discord-rich-presence = "1.1"
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "unstable-winit-030", "backend-winit", "renderer-femtovg-wgpu", "accessibility"], optional = true }
rfd = {version = "0.17", optional = true }
dirs = "6.0"
open = { version = "5.3", optional = true }
[target.'cfg(target_os = "android")'.dependencies]
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "backend-android-activity-06", "accessibility"], optional = true }
@@ -74,22 +74,10 @@ osx_frameworks = ["/opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib"]
osx_minimum_system_version = "15.0"
[package.metadata.android]
use_aapt2 = true
package = "io.github.gopher64.gopher64"
build_targets = ["aarch64-linux-android"]
resources = "data/android/res"
[[package.metadata.android.application.activity]]
name = "android.app.NativeActivity"
[[package.metadata.android.application.activity.intent_filter]]
actions = ["android.intent.action.MAIN"]
categories = ["android.intent.category.LAUNCHER"]
[[package.metadata.android.application.activity.meta_data]]
name = "android.app.lib_name"
value = "gopher64"
[package.metadata.android.application]
icon = "@mipmap/ic_launcher"
label = "Gopher64"
+2 -2
View File
@@ -293,7 +293,7 @@ pub async fn run() -> std::io::Result<()> {
#[cfg(feature = "gui")]
{
retroachievements::init_client(false, false, false);
ui::gui::app_window();
ui::gui::app_window(false);
retroachievements::shutdown_client();
}
}
@@ -317,5 +317,5 @@ async fn android_main(app: slint::android::AndroidApp) {
std::fs::create_dir_all(dirs.data_dir.join("saves")).unwrap();
std::fs::create_dir_all(dirs.data_dir.join("states")).unwrap();
ui::gui::app_window();
ui::gui::app_window(true);
}
+1 -1
View File
@@ -1,5 +1,5 @@
#[cfg(target_os = "android")]
pub mod android_input;
pub mod android;
pub mod audio;
#[cfg(feature = "gui")]
pub mod cheats;
+74 -1
View File
@@ -1,5 +1,43 @@
use jni::objects::JString;
use jni::refs::Global;
use jni::{Env, JavaVM, bind_java_type};
use crate::ui;
bind_java_type! {
AndroidActivity => "android.app.Activity",
type_map = {
AndroidIntent => "android.content.Intent",
},
methods {
fn start_activity(intent: AndroidIntent) -> (),
},
}
bind_java_type! {
AndroidIntent => "android.content.Intent",
type_map = {
AndroidUri => "android.net.Uri",
},
fields {
#[allow(non_snake_case)]
static ACTION_VIEW: JString,
},
constructors {
fn new(action: JString),
},
methods {
fn set_data(uri: AndroidUri) -> AndroidIntent,
},
}
bind_java_type! {
AndroidUri => "android.net.Uri",
methods {
static fn parse(uri_string: JString) -> AndroidUri,
},
}
bind_java_type! {
pub AndroidInputDevice => "android.view.InputDevice",
fields {
@@ -30,7 +68,7 @@ pub struct ControllerInfo {
/// Lists connected gamepads and joysticks using the Android framework.
pub fn list_controllers() -> Vec<ControllerInfo> {
let Some(app) = crate::ui::ANDROID_APP.get() else {
let Some(app) = ui::ANDROID_APP.get() else {
eprintln!("Android app not initialized; cannot list controllers");
return Vec::new();
};
@@ -104,3 +142,38 @@ fn list_controllers_on_jvm(env: &mut Env<'_>) -> jni::errors::Result<Vec<Control
Ok(controllers)
}
/// Opens a URI in the user's default app via [`Intent::ACTION_VIEW`](https://developer.android.com/reference/android/content/Intent#ACTION_VIEW).
pub fn open_uri(path: &str) {
let Some(app) = ui::ANDROID_APP.get() else {
eprintln!("Android app not initialized; cannot open URI");
return;
};
let path = path.to_string();
app.clone().run_on_java_main_thread(Box::new(move || {
let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) };
if let Err(err) = vm.attach_current_thread(|env| open_uri_on_java_main(env, &app, &path)) {
eprintln!("JNI error while opening URI: {err:?}");
}
}));
}
fn open_uri_on_java_main(
env: &mut Env<'_>,
app: &slint::android::AndroidApp,
path: &str,
) -> jni::errors::Result<()> {
let activity_ptr = app.activity_as_ptr().cast();
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&activity_ptr)? };
let uri_string = JString::from_str(env, path.to_string())?;
let uri = AndroidUri::parse(env, &uri_string)?;
let action_view = AndroidIntent::ACTION_VIEW(env)?;
let intent = AndroidIntent::new(env, &action_view)?;
intent.set_data(env, &uri)?;
activity.as_ref().start_activity(env, &intent)?;
Ok(())
}
+19 -11
View File
@@ -46,12 +46,18 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
});
}
pub fn open_uri(path: &str) {
if let Err(e) = robius_open::Uri::new(path).open() {
eprintln!("Error opening path: {:?}", e);
#[cfg(not(target_os = "android"))]
pub fn open_uri(path: impl AsRef<std::ffi::OsStr>) {
if let Err(e) = open::that_detached(path) {
eprintln!("Error opening path: {}", e);
}
}
#[cfg(target_os = "android")]
pub fn open_uri(path: impl AsRef<std::ffi::OsStr>) {
ui::android::open_uri(path.as_ref().to_str().unwrap())
}
fn run_with_path(weak: slint::Weak<AppWindow>, path: std::path::PathBuf) {
let weak2 = weak.clone();
weak.upgrade_in_event_loop(move |handle| {
@@ -88,8 +94,6 @@ fn file_dropped(app: &AppWindow) {
}
fn local_game_window(app: &AppWindow, config: &ui::config::Config) {
let dirs = ui::get_dirs();
app.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
config
@@ -131,12 +135,15 @@ fn local_game_window(app: &AppWindow, config: &ui::config::Config) {
.unwrap();
});
let saves_path = dirs.data_dir.join("saves");
app.on_saves_folder_button_clicked(move || {
open_uri(saves_path.to_str().unwrap());
});
#[cfg(not(target_os = "android"))]
file_dropped(app);
{
let saves_path = ui::get_dirs().data_dir.join("saves");
app.on_saves_folder_button_clicked(move || {
open_uri(&saves_path);
});
file_dropped(app);
}
}
fn input_profiles(config: &ui::config::Config) -> Vec<String> {
@@ -501,8 +508,9 @@ fn about_window(app: &AppWindow) {
}
}
pub fn app_window() {
pub fn app_window(is_android: bool) {
let app = AppWindow::new().unwrap();
app.set_is_android(is_android);
about_window(&app);
ui::retroachievements::ra_window(&app);
{
+1
View File
@@ -79,6 +79,7 @@ export component AppWindow inherits Window {
in-out property controller_paths <=> ControllerData.controller_paths;
in-out property selected_controller <=> ControllerData.selected_controller;
in-out property game_running <=> State.game_running;
in-out property is_android <=> State.is_android;
in-out property recent_roms <=> State.recent_roms;
in-out property cheat_game_name <=> CheatsData.cheat_game_name;
in-out property cheat_game_crc <=> CheatsData.cheat_game_crc;
+2 -1
View File
@@ -7,6 +7,7 @@ import { AboutData, NewVersionButton } from "about_page.slint";
export global State {
in-out property <bool> game_running;
in-out property <bool> is_android;
in-out property <[{a:string, b:string}]> recent_roms;
}
@@ -30,7 +31,7 @@ export component LocalGame inherits Page {
}
}
HorizontalBox {
if !State.is_android:HorizontalBox {
alignment: center;
Button {
text: @tr("Open Saves Folder");
+2 -2
View File
@@ -362,7 +362,7 @@ pub fn get_controller_names() -> Vec<String> {
#[cfg(target_os = "android")]
pub fn get_controller_names() -> Vec<String> {
let mut controllers: Vec<String> = ui::android_input::list_controllers()
let mut controllers: Vec<String> = ui::android::list_controllers()
.into_iter()
.map(|c| c.name)
.collect();
@@ -391,7 +391,7 @@ pub fn get_controller_paths() -> Vec<String> {
#[cfg(target_os = "android")]
#[cfg(feature = "gui")]
pub fn get_controller_paths() -> Vec<String> {
let mut controller_paths: Vec<String> = ui::android_input::list_controllers()
let mut controller_paths: Vec<String> = ui::android::list_controllers()
.into_iter()
.map(|c| c.descriptor)
.collect();
+1 -1
View File
@@ -100,7 +100,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) {
});
app.on_ra_show_profile_clicked(move || {
ui::gui::open_uri(&format!(
ui::gui::open_uri(format!(
"https://retroachievements.org/user/{}",
retroachievements::get_username().unwrap_or_default()
));