diff --git a/DOCUMENTATION/FORDEV.md b/DOCUMENTATION/FORDEV.md index 8eb767e..d38ee1a 100644 --- a/DOCUMENTATION/FORDEV.md +++ b/DOCUMENTATION/FORDEV.md @@ -64,4 +64,9 @@ Force-stop and clear app data: ```powershell & "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell am force-stop com.vyla.go & "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell pm clear com.vyla.go -``` \ No newline at end of file +``` + +Fix emulator DNS timeouts (if emulator gets network timeouts connecting to GitLab): +```powershell +& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell settings put global private_dns_mode off +``` diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7f2409f..2aba801 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4593,6 +4593,7 @@ dependencies = [ "tauri-plugin-opener", "tauri-plugin-process", "tauri-plugin-shell", + "tokio", "urlencoding", "zip", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 59d1a54..e7e93c8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,3 +23,4 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls" zip = { version = "2.2", default-features = false, features = ["deflate"] } futures-util = "0.3" urlencoding = "2.1" +tokio = { version = "1", features = ["time"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 02a3c49..32b30fe 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -352,44 +352,151 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind let base_dir = get_app_dir(&app); std::fs::create_dir_all(&base_dir).map_err(|e| format!("Storage dir error: {}", e))?; + let marker = base_dir.join(".installed"); let frontend_index = base_dir.join("frontend").join("index.html"); - if frontend_index.exists() { + let server_script = base_dir.join("player").join("server.js"); + if marker.exists() && frontend_index.exists() && server_script.exists() { + android_log("App already installed, launching..."); apply_patches(&base_dir); spawn_service_manager(&base_dir); update_ui(&window, "Launching Vyla Go...", Some(100)); return Ok(base_dir); } - update_ui(&window, "Connecting to GitLab...", Some(10)); + // 1. Check if a valid local archive already exists on device + let local_payload_candidates = [ + base_dir.join("update-payload.zip"), + base_dir.join("temp-payload.zip"), + PathBuf::from("/sdcard/Download/update-payload.zip"), + PathBuf::from("/data/local/tmp/update-payload.zip"), + ]; + + for candidate in &local_payload_candidates { + if candidate.exists() { + if let Ok(meta) = std::fs::metadata(candidate) { + if meta.len() > 10_000_000 { + if let Ok(f) = File::open(candidate) { + if let Ok(archive) = zip::ZipArchive::new(f) { + if archive.len() > 0 { + android_log(&format!("Found valid local payload: {}", candidate.display())); + update_ui(&window, "Unpacking local package...", Some(80)); + let extract_dir = base_dir.clone(); + let candidate_path = candidate.clone(); + let extract_ok = tauri::async_runtime::spawn_blocking(move || -> Result<(), String> { + let zip_file = File::open(&candidate_path).map_err(|e| format!("Open zip error: {}", e))?; + let mut archive = zip::ZipArchive::new(zip_file).map_err(|e| format!("Zip parse error: {}", e))?; + archive.extract(&extract_dir).map_err(|e| format!("Extraction error: {}", e))?; + let _ = std::fs::write(extract_dir.join(".installed"), "ok"); + apply_patches(&extract_dir); + Ok(()) + }) + .await + .map_err(|e| e.to_string())?; + + if extract_ok.is_ok() { + spawn_service_manager(&base_dir); + update_ui(&window, "Launching Vyla Go...", Some(100)); + return Ok(base_dir); + } + } + } + } + } + } + } + } let client = reqwest::Client::builder() .user_agent("Mozilla/5.0 (Android) VylaGo/1.0") - .connect_timeout(std::time::Duration::from_secs(30)) + .connect_timeout(std::time::Duration::from_secs(15)) + .timeout(std::time::Duration::from_secs(180)) .redirect(reqwest::redirect::Policy::limited(10)) .build() .map_err(|e| format!("Client error: {}", e))?; - let releases_url = "https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases"; - let res = client.get(releases_url).send().await.map_err(|e| format!("Release check failed: {}", e))?; - let json: serde_json::Value = res.json().await.map_err(|e| format!("JSON error: {}", e))?; + let release_endpoints = [ + "https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/releases", + "https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases", + ]; - let download_url = json.get(0) - .and_then(|r| r.get("assets")) - .and_then(|a| a.get("links")) - .and_then(|l| l.as_array()) - .and_then(|links| { - links.iter().find(|item| { - item.get("name").and_then(|n| n.as_str()) == Some("update-payload.zip") - }) - }) - .and_then(|asset| asset.get("url")) - .and_then(|u| u.as_str()) - .ok_or_else(|| "Could not locate update-payload.zip link in release assets".to_string())?; + let mut download_url: Option = None; + let mut last_error = String::new(); + + const MAX_RETRIES: u32 = 4; + for attempt in 1..=MAX_RETRIES { + let msg = if attempt == 1 { + "Connecting to GitLab...".to_string() + } else { + format!("Connecting to GitLab (attempt {}/{})...", attempt, MAX_RETRIES) + }; + update_ui(&window, &msg, Some(10 + (attempt - 1) * 3)); + + for endpoint in &release_endpoints { + android_log(&format!("Checking endpoint: {} (attempt {})", endpoint, attempt)); + match client.get(*endpoint).send().await { + Ok(res) => { + if res.status().is_success() { + if let Ok(json) = res.json::().await { + if let Some(releases) = json.as_array() { + for rel in releases { + if let Some(links) = rel.get("assets").and_then(|a| a.get("links")).and_then(|l| l.as_array()) { + for item in links { + if item.get("name").and_then(|n| n.as_str()) == Some("update-payload.zip") { + if let Some(url) = item.get("url").and_then(|u| u.as_str()) { + download_url = Some(url.to_string()); + break; + } + } + } + } + if download_url.is_some() { + break; + } + } + } + } + } else { + last_error = format!("HTTP {}", res.status()); + } + } + Err(e) => { + android_log(&format!("Release check failed for {}: {}", endpoint, e)); + last_error = format!("{}", e); + } + } + + if download_url.is_some() { + break; + } + } + + if download_url.is_some() { + break; + } + + if attempt < MAX_RETRIES { + let backoff = attempt * 2; + update_ui( + &window, + &format!("Connection pending, retrying in {}s...", backoff), + Some(10 + attempt * 3), + ); + tokio::time::sleep(std::time::Duration::from_secs(backoff as u64)).await; + } + } + + let download_url = download_url.ok_or_else(|| { + format!( + "Could not connect to release server ({})", + if last_error.is_empty() { "timeout" } else { &last_error } + ) + })?; update_ui(&window, "Downloading update payload...", Some(25)); android_log(&format!("Downloading from: {}", download_url)); - let response = client.get(download_url).send().await.map_err(|e| format!("Download error: {} (source: {:?})", e, StdError::source(&e)))?; android_log(&format!("Response status: {}", response.status())); + let response = client.get(&download_url).send().await.map_err(|e| format!("Download error: {} (source: {:?})", e, StdError::source(&e)))?; + android_log(&format!("Response status: {}", response.status())); let total_size = response.content_length().unwrap_or(0) as usize; let temp_zip_path = base_dir.join("temp-payload.zip"); @@ -408,7 +515,7 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind downloaded += chunk.len(); let percent = if total_size > 0 { - 10 + ((downloaded as f64 / total_size as f64) * 75.0) as u32 + 25 + ((downloaded as f64 / total_size as f64) * 65.0) as u32 } else { 35 }; @@ -431,7 +538,8 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind let zip_file = File::open(&zip_path_clone).map_err(|e| format!("Open zip error: {}", e))?; let mut archive = zip::ZipArchive::new(zip_file).map_err(|e| format!("Zip parse error: {}", e))?; archive.extract(&extract_dir).map_err(|e| format!("Extraction error: {}", e))?; - let _ = std::fs::remove_file(zip_path_clone); + let _ = std::fs::remove_file(&zip_path_clone); + let _ = std::fs::write(extract_dir.join(".installed"), "ok"); apply_patches(&extract_dir); Ok(()) }) @@ -554,25 +662,32 @@ pub fn run() { } tauri::async_runtime::spawn(async move { - std::thread::sleep(std::time::Duration::from_millis(400)); + std::thread::sleep(std::time::Duration::from_millis(600)); if let Some(window) = handle.get_webview_window("main") { update_ui(&window, "Initializing Vyla Go engine...", Some(5)); - match download_and_extract_payload(handle.clone(), window.clone()).await { - Ok(_) => { - std::thread::sleep(std::time::Duration::from_millis(400)); - let _ = window.eval(&format!( - "{}; window.location.replace('http://vyla.localhost/frontend/index.html');", - ANDROID_VIDEO_FIX_SCRIPT - )); - } - Err(err) => { - update_ui( - &window, - &format!("Error: {}", err), - Some(0), - ); + loop { + match download_and_extract_payload(handle.clone(), window.clone()).await { + Ok(_) => { + std::thread::sleep(std::time::Duration::from_millis(400)); + let _ = window.eval(&format!( + "{}; window.location.replace('http://vyla.localhost/frontend/index.html');", + ANDROID_VIDEO_FIX_SCRIPT + )); + break; + } + Err(err) => { + android_log(&format!("Installer error: {}", err)); + for remaining in (1..=8).rev() { + update_ui( + &window, + &format!("Error: {}. Retrying in {}s...", err, remaining), + Some(0), + ); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + } } } }