diff --git a/src/ui/install_wivrn_box.rs b/src/ui/install_wivrn_box.rs index 4c16b4f..92f8123 100644 --- a/src/ui/install_wivrn_box.rs +++ b/src/ui/install_wivrn_box.rs @@ -1,22 +1,61 @@ -use super::{ - alert::alert, - job_worker::{ - internal_worker::JobWorkerOut, - job::{FuncWorkerOut, WorkerJob}, - JobWorker, - }, -}; +use super::alert::alert; use crate::{ + async_process::async_process, dependencies::adb_dep::adb_dep, - downloader::download_file_sync, - paths::wivrn_apk_download_path, + downloader::{cache_file, cache_file_path}, profile::{Profile, XRServiceType}, }; -use gtk::{glib::clone, prelude::*}; +use gtk::prelude::*; use relm4::{new_action_group, new_stateless_action, prelude::*}; -use std::collections::VecDeque; +use std::fs::remove_file; -const DOWNLOAD_ERROR: &str = "download error"; +const ADB_ERR_NO_DEV: &str = "no devices/emulators found"; + +const WIVRN_LATEST_RELEASE_APK_URL: &str = + "https://github.com/WiVRn/WiVRn/releases/latest/download/WiVRn-standard-release.apk"; + +#[derive(Debug)] +enum GetWivrnApkRefErr { + NotWivrn, + RepoDirNotFound, + RepoManipulationFailed(git2::Error), +} + +#[derive(Debug, Clone)] +enum WivrnApkRef { + Tag(String), + Commit(String), +} + +fn get_wivrn_apk_ref(p: &Profile) -> Result { + if p.xrservice_type != XRServiceType::Wivrn { + Err(GetWivrnApkRefErr::NotWivrn) + } else if !p.xrservice_path.is_dir() { + Err(GetWivrnApkRefErr::RepoDirNotFound) + } else { + let repo = git2::Repository::open(&p.xrservice_path) + .map_err(GetWivrnApkRefErr::RepoManipulationFailed)?; + if let Ok(tag) = repo + .describe( + git2::DescribeOptions::new() + .describe_tags() + .max_candidates_tags(0), + ) + .and_then(|d| d.format(None)) + { + Ok(WivrnApkRef::Tag(tag)) + } else { + Ok(WivrnApkRef::Commit( + repo.head() + .map_err(GetWivrnApkRefErr::RepoManipulationFailed)? + .peel_to_commit() + .map_err(GetWivrnApkRefErr::RepoManipulationFailed)? + .id() + .to_string(), + )) + } + } +} #[derive(PartialEq, Eq, Debug, Clone)] pub enum InstallWivrnStatus { @@ -30,10 +69,6 @@ pub struct InstallWivrnBox { selected_profile: Profile, install_wivrn_status: InstallWivrnStatus, #[tracker::do_not_track] - install_runner: Option, - #[tracker::do_not_track] - install_runner_log: String, - #[tracker::do_not_track] root_win: gtk::Window, } @@ -41,9 +76,10 @@ pub struct InstallWivrnBox { #[derive(Debug)] pub enum InstallWivrnBoxMsg { UpdateSelectedProfile(Profile), - InstallWivrnApk(String), - OnInstallRunnerLog(Vec), - OnInstallRunnerExit(i32), + /// prepares state for async action, calls DoInstall + InstallWivrnApk, + /// dowloads, installs, sets state back to what it needs to be + DoInstall(String), } #[derive(Debug)] @@ -94,9 +130,7 @@ impl AsyncComponent for InstallWivrnBox { #[track = "model.changed(Self::install_wivrn_status())"] set_sensitive: model.install_wivrn_status != InstallWivrnStatus::InProgress, connect_clicked[sender] => move |_| { - sender.input(Self::Input::InstallWivrnApk( - "https://github.com/WiVRn/WiVRn/releases/latest/download/WiVRn-standard-release.apk".into() - )) + sender.input(Self::Input::InstallWivrnApk) }, }, gtk::Label { @@ -131,96 +165,98 @@ impl AsyncComponent for InstallWivrnBox { self.reset(); match message { - Self::Input::InstallWivrnApk(link) => { + Self::Input::InstallWivrnApk => { if !adb_dep().check() { alert("ADB is not installed", Some("Please install ADB on your computer to install WiVRn on your Android headset"), Some(&self.root_win)); return; } self.set_install_wivrn_status(InstallWivrnStatus::InProgress); - self.install_runner_log = String::default(); - let mut jobs = VecDeque::new(); - let wivrn_apk_path = wivrn_apk_download_path(); - jobs.push_back(WorkerJob::new_func(Box::new(clone!( - #[strong] - wivrn_apk_path, - move || { - if download_file_sync(&link, &wivrn_apk_path).is_err() { - FuncWorkerOut { - out: vec![DOWNLOAD_ERROR.into()], - success: false, - } - } else if wivrn_apk_path.is_file() { - FuncWorkerOut { - out: Vec::default(), - success: true, - } - } else { - FuncWorkerOut { - out: vec![format!( - "Provided apk path {} does not exist", - wivrn_apk_path.to_string_lossy() - )], - success: false, + + match get_wivrn_apk_ref(&self.selected_profile) { + Err(GetWivrnApkRefErr::NotWivrn) => { + eprintln!("This is not a WiVRn profile, how did you get here?"); + } + Err(GetWivrnApkRefErr::RepoDirNotFound) => { + self.set_install_wivrn_status(InstallWivrnStatus::Done(Some( + "Could not open WiVRn repository. Please build the profile before attempting to install the client APK".into() + ))); + } + Err(GetWivrnApkRefErr::RepoManipulationFailed(giterr)) => { + eprintln!("Error: failed to manipulate WiVRn repo: {giterr}, falling back to latest release APK"); + let existing = cache_file_path(WIVRN_LATEST_RELEASE_APK_URL, Some("apk")); + if existing.is_file() { + if let Err(e) = remove_file(&existing) { + eprintln!( + "Failed to remove file {}: {e}", + existing.to_string_lossy() + ); } } + sender.input(Self::Input::DoInstall(WIVRN_LATEST_RELEASE_APK_URL.into())); } - )))); - jobs.push_back(WorkerJob::new_cmd( - None, - "adb".into(), - Some(vec![ - "install".into(), - wivrn_apk_path.to_string_lossy().to_string(), - ]), - )); - let runner = JobWorker::new( - jobs, - sender.input_sender(), - Box::new(move |e| match e { - JobWorkerOut::Log(log) => Self::Input::OnInstallRunnerLog(log), - JobWorkerOut::Exit(code) => Self::Input::OnInstallRunnerExit(code), - }), - ); - runner.start(); - self.install_runner = Some(runner); + Ok(WivrnApkRef::Tag(tag)) => { + sender.input(Self::Input::DoInstall( + format!("https://github.com/WiVRn/WiVRn/releases/download/{tag}/WiVRn-standard-release.apk") + )); + } + Ok(WivrnApkRef::Commit(commit)) => { + sender.input(Self::Input::DoInstall( + format!("https://github.com/WiVRn/WiVRn-APK/releases/download/apk-${commit}/org.meumeu.wivrn-standard-release.apk") + )); + } + }; + } + Self::Input::DoInstall(url) => { + // TODO: we gonna cache or just download async every time? + match cache_file(&url, Some("apk")).await { + Err(e) => { + eprintln!("Failed to download apk: {e}"); + self.set_install_wivrn_status(InstallWivrnStatus::Done(Some( + "Error downloading WiVRn client APK".into(), + ))); + } + Ok(apk_path) => { + self.set_install_wivrn_status(match async_process( + "adb", + Some(&["install", apk_path.to_string_lossy().to_string().as_str()]), + None, + ) + .await + { + Ok(out) if out.exit_code == 0 => { + InstallWivrnStatus::Success + } + Ok(out) => { + if out.stdout.contains(ADB_ERR_NO_DEV) + || out.stderr.contains(ADB_ERR_NO_DEV) + { + InstallWivrnStatus::Done(Some( + concat!( + "No devices connected. Make sure your headset is connected ", + "to this computer and USB debugging is turned on." + ) + .into(), + )) + } else { + eprintln!("Error: ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr); + InstallWivrnStatus::Done(Some( + format!("ADB exited with code \"{}\"", out.exit_code) + )) + } + } + Err(e) => { + eprintln!("Error: failed to run ADB: {e}"); + InstallWivrnStatus::Done(Some( + "Failed to run ADB".into() + )) + } + }); + } + }; } Self::Input::UpdateSelectedProfile(p) => { self.set_selected_profile(p); } - Self::Input::OnInstallRunnerLog(rows) => { - self.install_runner_log.push('\n'); - self.install_runner_log.push_str(&rows.join("\n")); - } - Self::Input::OnInstallRunnerExit(code) => { - self.set_install_wivrn_status(match code { - 0 => InstallWivrnStatus::Success, - 255 => InstallWivrnStatus::Done(Some( - concat!( - "You need to authorize this computer to run developer ", - "commands in your headset. Authorize it and try again.", - ) - .into(), - )), - 1 if self.install_runner_log.contains(DOWNLOAD_ERROR) => { - InstallWivrnStatus::Done(Some("Error downloading WiVRn APK".into())) - } - 1 if self - .install_runner_log - .contains("no devices/emulators found") => - { - InstallWivrnStatus::Done(Some( - concat!( - "No devices connected. Make sure your headset is connected ", - "to this computer and USB debugging is turned on." - ) - .into(), - )) - } - _ => { - InstallWivrnStatus::Done(Some(format!("ADB exited with code \"{}\"", code))) - } - }); - } } } @@ -232,8 +268,6 @@ impl AsyncComponent for InstallWivrnBox { let model = Self { selected_profile: init.selected_profile, install_wivrn_status: InstallWivrnStatus::Done(None), - install_runner: None, - install_runner_log: String::default(), root_win: init.root_win, tracker: 0, }; @@ -246,4 +280,3 @@ impl AsyncComponent for InstallWivrnBox { new_action_group!(pub InstallWivrnActionGroup, "installwivrn"); new_stateless_action!(pub WivrnApkStandardAction, InstallWivrnActionGroup, "apkoculus"); -new_stateless_action!(pub WivrnApkPicoAction, InstallWivrnActionGroup, "apkpico");