stereokit_rust/tools/
os_api.rs

1use openxr_sys::pfn::EnumerateEnvironmentBlendModes;
2use openxr_sys::{EnvironmentBlendMode, Instance, Result, SystemId, ViewConfigurationType};
3use std::ffi::OsString;
4use std::fs::File;
5use std::path::Path;
6use std::path::PathBuf;
7use std::{cell::RefCell, rc::Rc};
8
9use crate::sk::SkInfo;
10use crate::system::{Backend, BackendOpenXR, BackendXRType, Log};
11
12/// When browsing files because of Android we need this API.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum PathEntry {
15    File(OsString),
16    Dir(OsString),
17}
18
19/// For bin tools only
20pub fn get_shaders_source_dir() -> String {
21    std::env::var("SK_RUST_SHADERS_SOURCE_DIR").unwrap_or("shaders_src".into())
22}
23
24/// Where sks shaders are store under assets dir. For bin tools and none android exe. For Android use app.asset_manager()
25pub fn get_shaders_sks_dir() -> String {
26    std::env::var("SK_RUST_SHADERS_SKS_DIR").unwrap_or("shaders".into())
27}
28
29/// For bin tools and non android exe. For Android use app.asset_manager()
30pub fn get_assets_dir() -> String {
31    std::env::var("SK_RUST_ASSETS_DIR").unwrap_or("assets".into())
32}
33
34/// Read all the assets of a given assets sub directory.
35/// * `sk_info` - The SkInfo smart pointer
36/// * `sub_dir` - The sub directory of the assets directory.
37/// * `file_extensions` - The file extensions to filter by.
38///
39/// Returns a vector of PathEntry.
40#[cfg(target_os = "android")]
41pub fn get_assets(
42    sk_info: &Option<Rc<RefCell<SkInfo>>>,
43    sub_dir: PathBuf,
44    file_extensions: &Vec<String>,
45) -> Vec<PathEntry> {
46    use std::ffi::CString;
47
48    if sk_info.is_none() {
49        Log::err("get_assets, sk_info is None");
50        return vec![];
51    }
52
53    let sk_i = sk_info.as_ref().unwrap().borrow_mut();
54    let app = sk_i.get_android_app();
55    let mut exts = vec![];
56    for extension in file_extensions {
57        let extension = extension[1..].to_string();
58        exts.push(OsString::from(extension));
59    }
60    let mut vec = vec![];
61    if let Ok(cstring) = CString::new(sub_dir.to_str().unwrap_or("Error!!!")) {
62        if let Some(asset_dir) = app.asset_manager().open_dir(cstring.as_c_str()) {
63            for entry in asset_dir {
64                if let Ok(entry_string) = entry.into_string() {
65                    let path = PathBuf::from(entry_string.clone());
66
67                    if exts.is_empty() {
68                        if let Some(file_name) = path.file_name() {
69                            vec.push(PathEntry::File(file_name.into()))
70                        } else {
71                            Log::err(format!("get_assets, path {:?} don't have a file_name", path));
72                        }
73                    } else if let Some(extension) = path.extension() {
74                        if exts.contains(&extension.to_os_string()) {
75                            if let Some(file_name) = path.file_name() {
76                                vec.push(PathEntry::File(file_name.into()))
77                            }
78                        }
79                    }
80                }
81            }
82        }
83    }
84    vec
85}
86
87/// Read all the assets of a given assets sub directory.
88/// * `sk_info` - The SkInfo smart pointer
89/// * `sub_dir` - The sub directory of the assets directory.
90/// * `file_extensions` - The file extensions to filter by.
91///
92/// Returns a vector of PathEntry.
93/// ### Examples
94/// ```
95/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
96/// use stereokit_rust::{tools::os_api::{get_assets, PathEntry}, include_asset_tree};
97///
98/// let sk_info  = Some(sk.get_sk_info_clone());
99///
100/// const ASSET_DIR: &[&str] = include_asset_tree!("assets");
101///
102/// let mut file_found = false;
103/// let exts = vec![".png".into(), ".jpg".into(), ".jpeg".into()];
104/// for dir_name_str in ASSET_DIR {
105///     let dir_name_str = if dir_name_str.starts_with("assets/") {
106///         &dir_name_str[7..] // remove "assets/" from the path
107///     } else {
108///         &dir_name_str[6..]  // remove "assets" from the path
109///     };
110///     println!("{} :", dir_name_str);
111///     let mut asset_sub_dir = std::path::PathBuf::new();
112///     asset_sub_dir.push(dir_name_str);
113///     for file in get_assets(&sk_info, asset_sub_dir, &exts) {
114///         println!("--- {:?}", file);
115///         if let PathEntry::File(file) = file {
116///             if file.into_string().unwrap_or_default()
117///                    .ends_with("log_viewer.png") { file_found = true}
118///         }
119///     }
120/// }
121/// assert!(file_found);
122/// ```
123#[cfg(not(target_os = "android"))]
124pub fn get_assets(
125    _sk_info: &Option<Rc<RefCell<SkInfo>>>,
126    sub_dir: PathBuf,
127    file_extensions: &Vec<String>,
128) -> Vec<PathEntry> {
129    use std::{env, fs::read_dir};
130
131    let sub_dir = sub_dir.to_str().unwrap_or("");
132    let mut exts = vec![];
133    for extension in file_extensions {
134        let extension = extension[1..].to_string();
135        exts.push(OsString::from(extension));
136    }
137
138    let path_text = env::current_dir().unwrap().to_owned().join(get_assets_dir());
139    let path_asset = path_text.join(sub_dir);
140    let mut vec = vec![];
141
142    if path_asset.exists() {
143        if path_asset.is_dir() {
144            match read_dir(&path_asset) {
145                Ok(read_dir) => {
146                    for file in read_dir.flatten() {
147                        let path = file.path();
148
149                        if file.path().is_file() {
150                            if exts.is_empty() {
151                                vec.push(PathEntry::File(file.file_name()))
152                            } else if let Some(extension) = path.extension()
153                                && (exts.is_empty() || exts.contains(&extension.to_os_string()))
154                            {
155                                vec.push(PathEntry::File(file.file_name()))
156                            }
157                        }
158                    }
159                }
160                Err(err) => {
161                    Log::diag(format!("Unable to read {path_asset:?}: {err}"));
162                }
163            }
164        } else {
165            Log::diag(format!("{path_asset:?} is not a dir"));
166        }
167    } else {
168        Log::diag(format!("{path_asset:?} do not exists"));
169    }
170
171    vec
172}
173
174/// Get the path to internal data directory for Android
175#[cfg(target_os = "android")]
176pub fn get_internal_path(sk_info: &Option<Rc<RefCell<SkInfo>>>) -> Option<PathBuf> {
177    if sk_info.is_none() {
178        Log::err("get_internal_path, sk_info is None");
179        return None;
180    }
181
182    let sk_i = sk_info.as_ref().unwrap().borrow_mut();
183    let app = sk_i.get_android_app();
184    app.internal_data_path()
185}
186
187/// Get the path to external data directory for non android
188#[cfg(not(target_os = "android"))]
189pub fn get_internal_path(_sk_info: &Option<Rc<RefCell<SkInfo>>>) -> Option<PathBuf> {
190    None
191}
192
193/// Get the path to external data directory for Android
194#[cfg(target_os = "android")]
195pub fn get_external_path(sk_info: &Option<Rc<RefCell<SkInfo>>>) -> Option<PathBuf> {
196    if sk_info.is_none() {
197        Log::err("get_external_path, sk_info is None");
198        return None;
199    }
200
201    let sk_i = sk_info.as_ref().unwrap().borrow_mut();
202    let app = sk_i.get_android_app();
203    app.external_data_path()
204}
205
206/// Get the path to external data directory for non android (assets)
207#[cfg(not(target_os = "android"))]
208pub fn get_external_path(_sk_info: &Option<Rc<RefCell<SkInfo>>>) -> Option<PathBuf> {
209    use std::env;
210
211    let path_assets = env::current_dir().unwrap().join(get_assets_dir());
212    Some(path_assets)
213}
214
215/// Open an asset like a file
216#[cfg(target_os = "android")]
217pub fn open_asset(sk_info: &Option<Rc<RefCell<SkInfo>>>, asset_path: impl AsRef<Path>) -> Option<File> {
218    use std::ffi::CString;
219
220    if sk_info.is_none() {
221        Log::err("open_asset, sk_info is None");
222        return None;
223    }
224
225    let sk_i = sk_info.as_ref().unwrap().borrow_mut();
226    let app = sk_i.get_android_app();
227
228    if let Ok(cstring) = CString::new(asset_path.as_ref().to_str().unwrap_or("Error!!!")) {
229        if let Some(asset) = app.asset_manager().open(cstring.as_c_str()) {
230            if let Ok(o_file_desc) = asset.open_file_descriptor() {
231                Some(File::from(o_file_desc.fd))
232            } else {
233                Log::err(format!("open_asset, {:?} cannot get a new file_descriptor", asset_path.as_ref()));
234                None
235            }
236        } else {
237            Log::err(format!("open_asset, path {:?} cannot be a opened", asset_path.as_ref()));
238            None
239        }
240    } else {
241        Log::err(format!("open_asset, path {:?} cannot be a cstring", asset_path.as_ref()));
242        None
243    }
244}
245
246/// Open an asset like a file
247/// * `sk_info` - The SkInfo smart pointer
248/// * `asset_path` - The path to the asset.
249///
250/// Returns a File if the asset was opened successfully, None otherwise.
251///
252/// ### Examples
253/// ```
254/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
255/// use stereokit_rust::tools::os_api::open_asset;
256/// use std::io::Read;
257///
258/// let sk_info  = Some(sk.get_sk_info_clone());
259///
260/// let asset_path = "textures/readme.md";
261///
262/// let mut file = open_asset(&sk_info, asset_path).expect("File readme should be opened");
263///
264/// let mut buffer = String::new();
265/// file.read_to_string(&mut buffer).expect("File readme should be read");
266/// assert!(buffer.starts_with("# Images"));
267/// ```
268#[cfg(not(target_os = "android"))]
269pub fn open_asset(_sk_info: &Option<Rc<RefCell<SkInfo>>>, asset_path: impl AsRef<Path>) -> Option<File> {
270    use std::env;
271
272    let path_assets = env::current_dir().unwrap().join(get_assets_dir());
273    let path_asset = path_assets.join(asset_path);
274    File::open(path_asset).ok()
275}
276
277/// Open and read an asset like a file
278#[cfg(target_os = "android")]
279pub fn read_asset(sk_info: &Option<Rc<RefCell<SkInfo>>>, asset_path: impl AsRef<Path>) -> Option<Vec<u8>> {
280    use std::ffi::CString;
281
282    if sk_info.is_none() {
283        Log::err("open_asset, sk_info is None");
284        return None;
285    }
286
287    let sk_i = sk_info.as_ref().unwrap().borrow_mut();
288    let app = sk_i.get_android_app();
289
290    if let Ok(cstring) = CString::new(asset_path.as_ref().to_str().unwrap_or("Error!!!")) {
291        if let Some(mut asset) = app.asset_manager().open(cstring.as_c_str()) {
292            if let Ok(o_buffer) = asset.buffer() {
293                Some(o_buffer.to_vec())
294            } else {
295                Log::err(format!("open_asset, {:?} cannot get the buffer", asset_path.as_ref()));
296                None
297            }
298        } else {
299            Log::err(format!("open_asset, path {:?} cannot be a opened", asset_path.as_ref()));
300            None
301        }
302    } else {
303        Log::err(format!("open_asset, path {:?} cannot be a cstring", asset_path.as_ref()));
304        None
305    }
306}
307
308/// Open and read an asset like a file
309/// * `sk_info` - The SkInfo smart pointer
310/// * `asset_path` - The path to the asset.
311///
312/// Returns a File if the asset was opened successfully, None otherwise.
313///
314/// ### Examples
315/// ```
316/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
317/// use stereokit_rust::tools::os_api::read_asset;
318/// use std::io::Read;
319///
320/// let sk_info  = Some(sk.get_sk_info_clone());
321///
322/// let asset_path = "textures/readme.md";
323///
324/// let buffer = read_asset(&sk_info, asset_path).expect("File readme should be readable");
325/// assert!(buffer.starts_with(b"# Images"));
326/// ```
327#[cfg(not(target_os = "android"))]
328pub fn read_asset(_sk_info: &Option<Rc<RefCell<SkInfo>>>, asset_path: impl AsRef<Path>) -> Option<Vec<u8>> {
329    use std::{env, io::Read};
330
331    let path_assets = env::current_dir().unwrap().join(get_assets_dir());
332    let path_asset = path_assets.join(&asset_path);
333    let mut fd = match File::open(path_asset).ok() {
334        Some(file) => file,
335        None => {
336            Log::err(format!("open_asset, path {:?} cannot be opened", asset_path.as_ref()));
337            return None;
338        }
339    };
340    let mut o_buffer = vec![];
341    match fd.read_to_end(&mut o_buffer) {
342        Ok(_) => Some(o_buffer),
343        Err(err) => {
344            Log::err(format!("open_asset, path {:?} cannot be read: {}", asset_path.as_ref(), err));
345            None
346        }
347    }
348}
349
350/// Read the files and eventually the sub directory of a given directory
351/// * `_sk_info` - The SkInfo smart pointer
352/// * `dir` - The directory to read.
353/// * `file_extensions` - The file extensions to filter by.
354/// * `show_other_dirs` - If true, the sub directories will be shown.
355///
356/// Returns a vector of PathEntry.
357/// ### Examples
358/// ```
359/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
360/// use stereokit_rust::tools::os_api::{get_files, PathEntry};
361///
362/// let sk_info  = Some(sk.get_sk_info_clone());
363///
364/// let mut file_found = false;
365/// let mut dir_found = false;
366/// let exts = vec![".png".into(), ".jpg".into(), ".jpeg".into()];
367/// let mut asset_sub_dir = std::path::PathBuf::new();
368/// asset_sub_dir.push("assets/textures");
369/// for file in get_files(&sk_info, asset_sub_dir, &exts, true) {
370///     println!("--- {:?}", file);
371///     match file {
372///         PathEntry::File(file) => {
373///             if file.into_string().unwrap_or_default()
374///                    .ends_with("log_viewer.jpeg") { file_found = true }
375///         }
376///         PathEntry::Dir(dir) => {
377///             if dir.into_string().unwrap_or_default()
378///                   .ends_with("water") { dir_found = true }
379///         }
380///     }
381/// }
382/// assert!(file_found);
383/// assert!(dir_found);
384/// ```
385pub fn get_files(
386    _sk_info: &Option<Rc<RefCell<SkInfo>>>,
387    dir: PathBuf,
388    file_extensions: &Vec<String>,
389    show_sub_dirs: bool,
390) -> Vec<PathEntry> {
391    use std::fs::read_dir;
392    let mut exts = vec![];
393    for extension in file_extensions {
394        let extension = extension[1..].to_string();
395        exts.push(OsString::from(extension));
396    }
397    let mut vec = vec![];
398
399    if dir.exists()
400        && dir.is_dir()
401        && let Ok(read_dir) = read_dir(dir)
402    {
403        for file in read_dir.flatten() {
404            let path = file.path();
405
406            if file.path().is_file() {
407                if exts.is_empty() {
408                    vec.push(PathEntry::File(file.file_name()))
409                } else if let Some(extension) = path.extension()
410                    && (exts.is_empty() || exts.contains(&extension.to_os_string()))
411                {
412                    vec.push(PathEntry::File(file.file_name()))
413                }
414            } else if show_sub_dirs && file.path().is_dir() {
415                vec.push(PathEntry::Dir(file.file_name()))
416            }
417        }
418    }
419    vec
420}
421
422/// Open winit IME keyboard. Does nothing on Quest
423#[cfg(target_os = "android")]
424pub fn show_soft_input_ime(sk_info: &Option<Rc<RefCell<SkInfo>>>, show: bool) -> bool {
425    if sk_info.is_none() {
426        Log::err("show_soft_input_ime, sk_info is None");
427        return false;
428    }
429
430    let sk_i = sk_info.as_ref().unwrap().borrow_mut();
431    let app = sk_i.get_android_app();
432    if show {
433        app.show_soft_input(false);
434    } else {
435        app.hide_soft_input(false);
436    }
437    true
438}
439/// Open nothing has we don't have a winit IME keyboard
440#[cfg(not(target_os = "android"))]
441pub fn show_soft_input_ime(_sk_info: &Option<Rc<RefCell<SkInfo>>>, _show: bool) -> bool {
442    false
443}
444
445/// Open Android IMS keyboard. This doesn't work for accentuated characters.
446#[cfg(target_os = "android")]
447pub fn show_soft_input(show: bool) -> bool {
448    use jni::objects::JValue;
449
450    let ctx = ndk_context::android_context();
451    let vm = match unsafe { jni::JavaVM::from_raw(ctx.vm() as _) } {
452        Ok(value) => value,
453        Err(e) => {
454            Log::err(format!("virtual_kbd : no vm !! : {:?}", e));
455            return false;
456        }
457    };
458    let activity = unsafe { jni::objects::JObject::from_raw(ctx.context() as _) };
459    let mut env = match vm.attach_current_thread() {
460        Ok(value) => value,
461        Err(e) => {
462            Log::err(format!("virtual_kbd : no env !! : {:?}", e));
463            return false;
464        }
465    };
466
467    let class_ctxt = match env.find_class("android/content/Context") {
468        Ok(value) => value,
469        Err(e) => {
470            Log::err(format!("virtual_kbd : no class_ctxt !! : {:?}", e));
471            return false;
472        }
473    };
474    let ims = match env.get_static_field(class_ctxt, "INPUT_METHOD_SERVICE", "Ljava/lang/String;") {
475        Ok(value) => value,
476        Err(e) => {
477            Log::err(format!("virtual_kbd : no ims !! : {:?}", e));
478            return false;
479        }
480    };
481
482    let im_manager = match env
483        .call_method(&activity, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;", &[ims.borrow()])
484        .unwrap()
485        .l()
486    {
487        Ok(value) => value,
488        Err(e) => {
489            Log::err(format!("virtual_kbd : no im_manager !! : {:?}", e));
490            return false;
491        }
492    };
493
494    let jni_window = match env.call_method(&activity, "getWindow", "()Landroid/view/Window;", &[]).unwrap().l() {
495        Ok(value) => value,
496        Err(e) => {
497            Log::err(format!("virtual_kbd : no jni_window !! : {:?}", e));
498            return false;
499        }
500    };
501
502    let view = match env.call_method(jni_window, "getDecorView", "()Landroid/view/View;", &[]).unwrap().l() {
503        Ok(value) => value,
504        Err(e) => {
505            Log::err(format!("virtual_kbd : no view !! : {:?}", e));
506            return false;
507        }
508    };
509
510    if show {
511        let result = env
512            .call_method(im_manager, "showSoftInput", "(Landroid/view/View;I)Z", &[JValue::Object(&view), 0i32.into()])
513            .unwrap()
514            .z()
515            .unwrap();
516        result
517    } else {
518        let window_token = env.call_method(view, "getWindowToken", "()Landroid/os/IBinder;", &[]).unwrap().l().unwrap();
519        let jvalue_window_token = jni::objects::JValueGen::Object(&window_token);
520
521        let result = env
522            .call_method(
523                im_manager,
524                "hideSoftInputFromWindow",
525                "(Landroid/os/IBinder;I)Z",
526                &[jvalue_window_token, 0i32.into()],
527            )
528            .unwrap()
529            .z()
530            .unwrap();
531        result
532    }
533}
534
535/// Open nothing has we don't have a virtual keyboard
536#[cfg(not(target_os = "android"))]
537pub fn show_soft_input(_show: bool) -> bool {
538    false
539}
540
541/// Open the default browser. Adapted from https://github.com/amodm/webbrowser-rs
542#[cfg(target_os = "android")]
543pub fn launch_browser_android(url: &str) -> bool {
544    use jni::objects::{JObject, JValue};
545
546    Log::diag(format!("launch_browser_android: Attempting to open URL: {}", url));
547
548    // Create a VM for executing Java calls
549    let ctx = ndk_context::android_context();
550    let vm = match unsafe { jni::JavaVM::from_raw(ctx.vm() as _) } {
551        Ok(value) => value,
552        Err(e) => {
553            Log::err(format!("launch_browser_android: no vm !! : {:?}", e));
554            return false;
555        }
556    };
557
558    let activity = unsafe { jni::objects::JObject::from_raw(ctx.context() as _) };
559    let mut env = match vm.attach_current_thread() {
560        Ok(value) => value,
561        Err(e) => {
562            Log::err(format!("launch_browser_android: no env !! : {:?}", e));
563            return false;
564        }
565    };
566
567    // Create ACTION_VIEW object
568    let intent_class = match env.find_class("android/content/Intent") {
569        Ok(value) => value,
570        Err(e) => {
571            Log::err(format!("launch_browser_android: no intent_class !! : {:?}", e));
572            return false;
573        }
574    };
575    let action_view = match env.get_static_field(&intent_class, "ACTION_VIEW", "Ljava/lang/String;") {
576        Ok(value) => value,
577        Err(e) => {
578            Log::err(format!("launch_browser_android: no action_view !! : {:?}", e));
579            return false;
580        }
581    };
582
583    // Create Uri object
584    let uri_class = match env.find_class("android/net/Uri") {
585        Ok(value) => value,
586        Err(e) => {
587            Log::err(format!("launch_browser_android: no uri_class !! : {:?}", e));
588            return false;
589        }
590    };
591    let url_string = match env.new_string(url) {
592        Ok(value) => value,
593        Err(e) => {
594            Log::err(format!("launch_browser_android: no url_string !! : {:?}", e));
595            return false;
596        }
597    };
598    let uri = match env
599        .call_static_method(
600            &uri_class,
601            "parse",
602            "(Ljava/lang/String;)Landroid/net/Uri;",
603            &[JValue::Object(&JObject::from(url_string))],
604        )
605        .unwrap()
606        .l()
607    {
608        Ok(value) => value,
609        Err(e) => {
610            Log::err(format!("launch_browser_android: no uri !! : {:?}", e));
611            return false;
612        }
613    };
614
615    // Create new ACTION_VIEW intent with the uri
616    let intent = match env.alloc_object(&intent_class) {
617        Ok(value) => value,
618        Err(e) => {
619            Log::err(format!("launch_browser_android: no intent !! : {:?}", e));
620            return false;
621        }
622    };
623    if let Err(e) = env.call_method(
624        &intent,
625        "<init>",
626        "(Ljava/lang/String;Landroid/net/Uri;)V",
627        &[action_view.borrow(), JValue::Object(&uri)],
628    ) {
629        Log::err(format!("launch_browser_android: intent init failed !! : {:?}", e));
630        return false;
631    }
632
633    // Start the intent activity.
634    match env.call_method(&activity, "startActivity", "(Landroid/content/Intent;)V", &[JValue::Object(&intent)]) {
635        Ok(_) => {
636            // Just clear any pending exceptions without detailed analysis
637            if env.exception_check().unwrap_or(false) {
638                let _ = env.exception_clear();
639                Log::err("launch_browser_android: Activity exception occurred (cleared)");
640                return false;
641            }
642            true
643        }
644        Err(e) => {
645            Log::err(format!("launch_browser_android: startActivity failed: {} | URL: {}", e, url));
646            // Clear any pending exceptions without trying to read them
647            if env.exception_check().unwrap_or(false) {
648                let _ = env.exception_clear();
649            }
650            false
651        }
652    }
653}
654
655/// Open nothing has we don't have a virtual keyboard
656#[cfg(not(target_os = "android"))]
657pub fn launch_browser_android(_url: &str) -> bool {
658    false
659}
660
661/// Supported system deep link actions for Meta Quest following official Meta specifications
662#[derive(Debug, Clone, PartialEq)]
663pub enum SystemAction {
664    /// Open the browser with a URL
665    /// Intent: systemux://browser, URI: [any valid URL]
666    Browser { url: String },
667    /// Open the store
668    /// Intent: systemux://store, URI: [none] for front page or /item/[ID] for specific app
669    Store { app_id: Option<String> },
670    /// Open settings
671    /// Intent: systemux://settings, URI options:
672    /// - [none]: Main settings page
673    /// - /hands: Hand tracking settings
674    /// - /system: System settings
675    /// - /privacy: Privacy settings
676    /// - /controllers: Controllers settings
677    /// - /bluetooth: Bluetooth settings
678    /// - /wifi: WiFi settings
679    /// - /device: Device settings
680    /// - /guardian: Guardian/Boundary settings
681    /// - /accounts: Accounts settings
682    /// - /notifications: Notifications settings
683    /// - /applications?package=com.X.Y: Settings for specific app
684    Settings { setting: Option<String> },
685    /// Open Files app
686    /// Intent: systemux://file-manager, URI: [none] for Recents, /media/ for Media tab, /downloads/ for Downloads tab
687    FileManager { path: Option<String> },
688    /// Open Meta bug reporter
689    /// Intent: systemux://bug_report, URI: N/A
690    BugReport,
691}
692
693/// System Deep Link function for Meta Quest following Meta specifications.
694/// This function uses the VR Shell package to trigger various system actions.
695///
696/// # Arguments
697/// * `action` - The system action to perform
698///
699/// # Examples
700/// ```
701/// // Open browser with URL
702/// system_deep_link(SystemAction::Browser("https://www.oculus.com".to_string()));
703///
704/// // Open store with app ID
705/// system_deep_link(SystemAction::Store("1234567890".to_string()));
706///
707/// // Open home screen
708/// system_deep_link(SystemAction::Home);
709///
710/// // Custom action
711/// system_deep_link(SystemAction::Custom {
712///     action: "myaction".to_string(),
713///     data: Some("mydata".to_string())
714/// });
715/// ```
716/// System Deep Link function for Meta Quest following Meta specifications.
717/// This function uses the VR Shell package to trigger various system actions.
718///
719/// # Arguments
720/// * `action` - The system action to perform
721///
722/// # Examples
723/// ```
724/// // Open browser with URL
725/// system_deep_link(SystemAction::Browser("https://www.oculus.com".to_string()));
726///
727/// // Open store with app ID
728/// system_deep_link(SystemAction::Store("1234567890".to_string()));
729///
730/// // Open home screen
731/// system_deep_link(SystemAction::Home);
732///
733/// // Custom action
734/// System Deep Link function for Meta Quest following official Meta specifications.
735/// This function uses PackageManager.getLaunchIntentForPackage() with intent_data and uri extras
736/// as documented in Meta's System Deep Linking guide.
737///
738/// # Arguments
739/// * `action` - The system action to perform
740///
741/// # Examples
742/// ```
743/// // Open browser with URL
744/// system_deep_link(SystemAction::Browser { url: "https://www.meta.com".to_string() });
745///
746/// // Open store front page
747/// system_deep_link(SystemAction::Store { app_id: None });
748///
749/// // Open store page for specific app
750/// system_deep_link(SystemAction::Store { app_id: Some("1234567890".to_string()) });
751///
752/// // Open settings main page
753/// system_deep_link(SystemAction::Settings { setting: None });
754///
755/// // Open hand tracking settings
756/// system_deep_link(SystemAction::Settings { setting: Some("/hands".to_string()) });
757///
758/// // Open controllers settings
759/// system_deep_link(SystemAction::Settings { setting: Some("/controllers".to_string()) });
760///
761/// // Open WiFi settings
762/// system_deep_link(SystemAction::Settings { setting: Some("/wifi".to_string()) });
763///
764/// // Open Guardian/Boundary settings
765/// system_deep_link(SystemAction::Settings { setting: Some("/guardian".to_string()) });
766///
767/// // Open settings for specific app
768/// system_deep_link(SystemAction::Settings { setting: Some("/applications?package=com.oculus.browser".to_string()) });
769///
770/// // Open bug reporter
771/// system_deep_link(SystemAction::BugReport);
772/// ```
773#[cfg(target_os = "android")]
774pub fn system_deep_link(action: SystemAction) -> bool {
775    use crate::system::Log;
776    use jni::objects::JValue;
777
778    // Create a VM for executing Java calls
779    let ctx = ndk_context::android_context();
780    let vm = match unsafe { jni::JavaVM::from_raw(ctx.vm() as _) } {
781        Ok(value) => value,
782        Err(e) => {
783            Log::err(format!("system_deep_link: Failed to get VM: {:?}", e));
784            return false;
785        }
786    };
787
788    let activity = unsafe { jni::objects::JObject::from_raw(ctx.context() as _) };
789    let mut env = match vm.attach_current_thread() {
790        Ok(env) => env,
791        Err(e) => {
792            Log::err(format!("system_deep_link: Failed to attach to JVM thread: {:?}", e));
793            return false;
794        }
795    };
796
797    // Prepare action info according to Meta specifications
798    let (intent_data, uri_value, display_data) = match &action {
799        SystemAction::Browser { url } => {
800            ("systemux://browser", url.clone(), format!("Opening browser with URL: {}", url))
801        }
802        SystemAction::Store { app_id } => {
803            let uri = match app_id {
804                Some(id) => format!("/item/{}", id),
805                None => String::new(),
806            };
807            (
808                "systemux://store",
809                uri,
810                format!(
811                    "Opening store{}",
812                    if app_id.is_some() { format!(" for app: {}", app_id.as_ref().unwrap()) } else { String::new() }
813                ),
814            )
815        }
816        SystemAction::Settings { setting } => {
817            let uri = setting.as_deref().unwrap_or("");
818            (
819                "systemux://settings",
820                uri.to_string(),
821                format!("Opening settings{}", if !uri.is_empty() { format!(": {}", uri) } else { String::new() }),
822            )
823        }
824        SystemAction::FileManager { path } => {
825            let uri = path.as_deref().unwrap_or("");
826            (
827                "systemux://file-manager",
828                uri.to_string(),
829                format!("Opening file manager{}", if !uri.is_empty() { format!(": {}", uri) } else { String::new() }),
830            )
831        }
832        SystemAction::BugReport => ("systemux://bug_report", String::new(), "Opening bug report".to_string()),
833    };
834
835    Log::info(format!("system_deep_link: {}", display_data));
836    Log::diag(format!(
837        "system_deep_link: Attempting to launch VR Shell with intent_data='{}', uri='{}'",
838        intent_data, uri_value
839    ));
840
841    // Get PackageManager from context (following Meta specification)
842    let package_manager =
843        match env.call_method(&activity, "getPackageManager", "()Landroid/content/pm/PackageManager;", &[]) {
844            Ok(pm) => match pm.l() {
845                Ok(pm_obj) => pm_obj,
846                Err(e) => {
847                    Log::err(format!("system_deep_link: Failed to extract PackageManager object: {}", e));
848                    return false;
849                }
850            },
851            Err(e) => {
852                Log::err(format!("system_deep_link: Failed to get PackageManager: {}", e));
853                return false;
854            }
855        };
856
857    // Get launch intent for VR Shell (following Meta specification)
858    let package_name = match env.new_string("com.oculus.vrshell") {
859        Ok(s) => s,
860        Err(e) => {
861            Log::err(format!("system_deep_link: Failed to create package name string: {}", e));
862            return false;
863        }
864    };
865
866    let intent = match env.call_method(
867        &package_manager,
868        "getLaunchIntentForPackage",
869        "(Ljava/lang/String;)Landroid/content/Intent;",
870        &[JValue::Object(&package_name.into())],
871    ) {
872        Ok(intent_result) => match intent_result.l() {
873            Ok(intent_obj) if !intent_obj.is_null() => intent_obj,
874            Ok(_) => {
875                Log::err("system_deep_link: getLaunchIntentForPackage returned null");
876                return false;
877            }
878            Err(e) => {
879                Log::err(format!("system_deep_link: Failed to extract Intent object: {}", e));
880                return false;
881            }
882        },
883        Err(e) => {
884            Log::err(format!("system_deep_link: Failed to get launch intent: {}", e));
885            return false;
886        }
887    };
888
889    // Add intent_data extra (following Meta specification)
890    let intent_data_key = match env.new_string("intent_data") {
891        Ok(s) => s,
892        Err(e) => {
893            Log::err(format!("system_deep_link: Failed to create intent_data key: {}", e));
894            return false;
895        }
896    };
897
898    let intent_data_value = match env.new_string(intent_data) {
899        Ok(s) => s,
900        Err(e) => {
901            Log::err(format!("system_deep_link: Failed to create intent_data value: {}", e));
902            return false;
903        }
904    };
905
906    if let Err(e) = env.call_method(
907        &intent,
908        "putExtra",
909        "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;",
910        &[JValue::Object(&intent_data_key.into()), JValue::Object(&intent_data_value.into())],
911    ) {
912        Log::err(format!("system_deep_link: Failed to add intent_data extra: {}", e));
913        return false;
914    }
915
916    // Add uri extra if not empty (following Meta specification)
917    if !uri_value.is_empty() {
918        let uri_key = match env.new_string("uri") {
919            Ok(s) => s,
920            Err(e) => {
921                Log::err(format!("system_deep_link: Failed to create uri key: {}", e));
922                return false;
923            }
924        };
925
926        let uri_value_string = match env.new_string(&uri_value) {
927            Ok(s) => s,
928            Err(e) => {
929                Log::err(format!("system_deep_link: Failed to create uri value: {}", e));
930                return false;
931            }
932        };
933
934        if let Err(e) = env.call_method(
935            &intent,
936            "putExtra",
937            "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;",
938            &[JValue::Object(&uri_key.into()), JValue::Object(&uri_value_string.into())],
939        ) {
940            Log::err(format!("system_deep_link: Failed to add uri extra: {}", e));
941            return false;
942        }
943    }
944
945    // Start activity (following Meta specification)
946    match env.call_method(&activity, "startActivity", "(Landroid/content/Intent;)V", &[JValue::Object(&intent)]) {
947        Ok(_) => {
948            // Just clear any pending exceptions without detailed analysis
949            if env.exception_check().unwrap_or(false) {
950                let _ = env.exception_clear();
951                Log::err("system_deep_link: Activity exception occurred (cleared)");
952                return false;
953            }
954            Log::info(format!("system_deep_link: Successfully executed: {}", display_data));
955            true
956        }
957        Err(e) => {
958            Log::err(format!(
959                "system_deep_link: Failed to start activity: {} | Action: {:?} | Intent data: {} | URI: {}",
960                e, action, intent_data, uri_value
961            ));
962            // Clear any pending exceptions without trying to read them
963            if env.exception_check().unwrap_or(false) {
964                let _ = env.exception_clear();
965            }
966            false
967        }
968    }
969}
970
971#[cfg(not(target_os = "android"))]
972pub fn system_deep_link(_action: SystemAction) -> bool {
973    use crate::system::Log;
974    Log::warn("system_deep_link: Not supported on non-Android platforms");
975    false
976}
977
978/// Get the list of environnement blend_modes available on this device.
979/// * `with_log` - if true, will log the available blend modes.
980///
981/// see also [`crate::util::Device::valid_blend`]
982///
983/// ### Examples
984/// ```
985/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
986///
987/// use stereokit_rust::{tools::os_api::get_env_blend_modes, util::Device, sk::DisplayBlend};
988/// use openxr_sys::EnvironmentBlendMode;
989///
990/// let blend_modes = get_env_blend_modes(true);
991/// if blend_modes.len() > 0 {
992///     assert!(blend_modes.contains(&EnvironmentBlendMode::OPAQUE));
993///     if blend_modes.contains(&EnvironmentBlendMode::ADDITIVE)
994///     || blend_modes.contains(&EnvironmentBlendMode::ALPHA_BLEND)
995///     {
996///        println!("Passthrough available !!");
997///        // we can activate passthrough:
998///        Device::display_blend(DisplayBlend::AnyTransparent);
999///     }
1000/// } else {
1001///     // Simplest way to check if passthrough is available:
1002///     assert_eq!(Device::valid_blend(DisplayBlend::AnyTransparent), false);
1003/// }
1004///
1005/// test_steps!( // !!!! Get a proper main loop !!!!
1006///     if iter == 0 {
1007///         // activate passthrough if available
1008///         Device::display_blend(DisplayBlend::AnyTransparent);
1009///     } else if iter == 1 {
1010///         // deactivate passthrough
1011///         Device::display_blend(DisplayBlend::Opaque);
1012///     }
1013/// );
1014/// ```
1015pub fn get_env_blend_modes(with_log: bool) -> Vec<EnvironmentBlendMode> {
1016    //>>>>>>>>>>> Get the env blend mode
1017    let mut count = 0u32;
1018    let mut modes = [EnvironmentBlendMode::OPAQUE; 20];
1019    if Backend::xr_type() != BackendXRType::OpenXR {
1020        return vec![];
1021    }
1022    if let Some(get_modes) =
1023        BackendOpenXR::get_function::<EnumerateEnvironmentBlendModes>("xrEnumerateEnvironmentBlendModes")
1024    {
1025        match unsafe {
1026            get_modes(
1027                Instance::from_raw(BackendOpenXR::instance()),
1028                SystemId::from_raw(BackendOpenXR::system_id()),
1029                ViewConfigurationType::PRIMARY_STEREO,
1030                0,
1031                &mut count,
1032                modes.as_mut_ptr(),
1033            )
1034        } {
1035            Result::SUCCESS => {
1036                if with_log {
1037                    if count > 20 {
1038                        count = 20
1039                    }
1040                    match unsafe {
1041                        get_modes(
1042                            Instance::from_raw(BackendOpenXR::instance()),
1043                            SystemId::from_raw(BackendOpenXR::system_id()),
1044                            ViewConfigurationType::PRIMARY_STEREO,
1045                            count,
1046                            &mut count,
1047                            modes.as_mut_ptr(),
1048                        )
1049                    } {
1050                        Result::SUCCESS => {
1051                            if with_log {
1052                                Log::info(format!("✅ There are {count} env blend modes:"));
1053                                for (i, iter) in modes.iter().enumerate() {
1054                                    if i >= count as usize {
1055                                        break;
1056                                    }
1057                                    Log::info(format!("   {iter:?} "));
1058                                }
1059                            }
1060                        }
1061                        otherwise => {
1062                            if with_log {
1063                                Log::err(format!("❌ xrEnumerateEnvironmentBlendModes failed: {otherwise}"));
1064                            }
1065                        }
1066                    }
1067                }
1068            }
1069            otherwise => {
1070                if with_log {
1071                    Log::err(format!("❌ xrEnumerateEnvironmentBlendModes failed: {otherwise}"));
1072                }
1073            }
1074        }
1075    } else {
1076        Log::err("❌ xrEnumerateEnvironmentBlendModes binding function error !");
1077    }
1078    modes[0..(count as usize)].into()
1079}