//! This module hooks up [AppDelegate]s to arbitrary implementations of [AppDownloader] and //! [fetch::ProgressUpdater]. use crate::{ delegate::{AppDelegate, AppDelegateQueue}, resource, }; use mullvad_update::{ app::{self, AppDownloader, AppDownloaderParameters, DownloadedInstaller, VerifiedInstaller}, fetch, }; /// [AppDownloader] that delegates the actual work to some underlying `downloader` and uses it to /// update a UI. pub struct UiAppDownloader { downloader: Downloader, /// Queue used to control the app UI queue: Delegate::Queue, } /// Parameters for [UiAppDownloader] pub type UiAppDownloaderParameters = AppDownloaderParameters>; impl UiAppDownloader { /// Construct a [UiAppDownloader]. pub fn new(delegate: &Delegate, downloader: Downloader) -> Self { Self { downloader, queue: delegate.queue(), } } } impl AppDownloader for UiAppDownloader { async fn download_executable(self) -> Result { match self.downloader.download_executable().await { Ok(installer) => { self.queue.queue_main(move |self_| { self_.set_download_text(resource::DOWNLOAD_COMPLETE_DESC); self_.disable_cancel_button(); }); Ok(UiAppDownloader:: { downloader: installer, queue: self.queue, }) } Err(err) => { self.queue.queue_main(move |self_| { self_.clear_status_text(); self_.clear_download_text(); self_.hide_download_progress(); self_.hide_download_button(); self_.hide_cancel_button(); self_.show_error_message(crate::delegate::ErrorMessage { status_text: resource::DOWNLOAD_FAILED_DESC.to_owned(), cancel_button_text: resource::DOWNLOAD_FAILED_CANCEL_BUTTON_TEXT.to_owned(), retry_button_text: resource::DOWNLOAD_FAILED_RETRY_BUTTON_TEXT.to_owned(), }); }); Err(err) } } } } impl DownloadedInstaller for UiAppDownloader { async fn verify(self) -> Result { match self.downloader.verify().await { Ok(verified) => { self.queue.queue_main(move |self_| { self_.set_download_text(resource::VERIFICATION_SUCCEEDED_DESC); }); Ok(UiAppDownloader:: { downloader: verified, queue: self.queue, }) } Err(error) => { self.queue.queue_main(move |self_| { self_.clear_status_text(); self_.clear_download_text(); self_.hide_download_progress(); self_.hide_download_button(); self_.hide_cancel_button(); self_.show_error_message(crate::delegate::ErrorMessage { status_text: resource::VERIFICATION_FAILED_DESC.to_owned(), cancel_button_text: resource::VERIFICATION_FAILED_CANCEL_BUTTON_TEXT .to_owned(), retry_button_text: resource::VERIFICATION_FAILED_RETRY_BUTTON_TEXT .to_owned(), }); }); Err(error) } } } fn version(&self) -> &mullvad_version::Version { self.downloader.version() } } impl VerifiedInstaller for UiAppDownloader { async fn install(self) -> Result<(), app::DownloadError> { match self.downloader.install().await { Ok(()) => { self.queue.queue_main(move |self_| { // Success! self_.quit(); }); Ok(()) } Err(error) => { self.queue.queue_main(move |self_| { self_.clear_status_text(); self_.clear_download_text(); self_.hide_download_progress(); self_.hide_download_button(); self_.hide_cancel_button(); self_.show_error_message(crate::delegate::ErrorMessage { status_text: resource::LAUNCH_FAILED_DESC.to_owned(), cancel_button_text: resource::LAUNCH_FAILED_CANCEL_BUTTON_TEXT.to_owned(), retry_button_text: resource::LAUNCH_FAILED_RETRY_BUTTON_TEXT.to_owned(), }); }); Err(error) } } } } /// Implementation of [fetch::ProgressUpdater] that updates some [AppDelegate]. pub struct UiProgressUpdater { domain: Option, prev_progress: Option, queue: Delegate::Queue, } impl UiProgressUpdater { pub fn new(queue: Delegate::Queue) -> Self { Self { domain: None, prev_progress: None, queue, } } fn need_update(&mut self, complete: u32) -> bool { if self.prev_progress == Some(complete) { // Unconditionally updating causes flickering return false; } self.prev_progress = Some(complete); true } fn complete_from_percentage(fraction_complete: f32) -> u32 { (100.0 * fraction_complete).min(100.0) as u32 } fn status_text(&self, complete_percentage: u32) -> String { format!( "{} {}... ({complete_percentage}%)", resource::DOWNLOADING_DESC_PREFIX, self.domain() ) } fn domain(&self) -> &str { self.domain.as_deref().unwrap_or("unknown source") } } impl fetch::ProgressUpdater for UiProgressUpdater { fn set_progress(&mut self, fraction_complete: f32) { let value = Self::complete_from_percentage(fraction_complete); if !self.need_update(value) { return; } let status = self.status_text(value); self.queue.queue_main(move |self_| { self_.set_download_progress(value); self_.set_download_text(&status); }); } fn clear_progress(&mut self) { let value = 0; if !self.need_update(value) { return; } let status = self.status_text(value); self.queue.queue_main(move |self_| { self_.clear_download_progress(); self_.set_download_text(&status); }); } fn set_url(&mut self, url: &str) { // Parse out domain name let url = url.strip_prefix("https://").unwrap_or(url); let (domain, _) = url.split_once('/').unwrap_or((url, "")); self.domain = Some(domain.to_owned()); } }