diff --git a/ADB.cs b/ADB.cs index df0b38e..834a41d 100644 --- a/ADB.cs +++ b/ADB.cs @@ -154,7 +154,7 @@ namespace AndroidSideloader // Copies and installs an APK with real-time progress reporting using AdvancedSharpAdbClient public static async Task SideloadWithProgressAsync( string path, - Action progressCallback = null, // Now includes ETA + Action progressCallback = null, Action statusCallback = null, string packagename = "", string gameName = "") @@ -177,112 +177,79 @@ namespace AndroidSideloader // Throttle UI updates to prevent lag DateTime lastProgressUpdate = DateTime.MinValue; - int lastReportedPercent = -1; - const int ThrottleMs = 100; // Update UI at most every 100ms + float lastReportedPercent = -1; + const int ThrottleMs = 100; // Update UI every 100ms - // ETA tracking with smoothing - DateTime installStart = DateTime.UtcNow; - int etaLastPercent = 0; - DateTime etaLastPercentTime = DateTime.UtcNow; - double smoothedSecondsPerPercent = 0; - TimeSpan? lastReportedEta = null; - const double SmoothingAlpha = 0.15; // Lower = smoother, less responsive - const double EtaChangeThreshold = 0.10; // Only update if ETA changes by >10% + // Shared ETA engine (percent-units) + var eta = new EtaEstimator(alpha: 0.05, reanchorThreshold: 0.20); // Create install progress handler Action installProgress = (args) => { - int percent = 0; + float percent = 0; string status = null; - TimeSpan? eta = null; + TimeSpan? displayEta = null; switch (args.State) { case PackageInstallProgressState.Preparing: percent = 0; status = "Preparing..."; - installStart = DateTime.UtcNow; - etaLastPercent = 0; - etaLastPercentTime = installStart; - smoothedSecondsPerPercent = 0; - lastReportedEta = null; + eta.Reset(); break; + case PackageInstallProgressState.Uploading: - percent = (int)Math.Round(args.UploadProgress); + percent = (float)args.UploadProgress; - // Calculate ETA with smoothing - if (percent > etaLastPercent && percent < 100) + // Update ETA engine using percent as units (0..100) + if (percent > 0 && percent < 100) { - var now = DateTime.UtcNow; - double secondsForThisChunk = (now - etaLastPercentTime).TotalSeconds; - int percentGained = percent - etaLastPercent; - - if (percentGained > 0 && secondsForThisChunk > 0) - { - double secondsPerPercent = secondsForThisChunk / percentGained; - - // Exponential smoothing - if (smoothedSecondsPerPercent == 0) - smoothedSecondsPerPercent = secondsPerPercent; - else - smoothedSecondsPerPercent = SmoothingAlpha * secondsPerPercent + (1 - SmoothingAlpha) * smoothedSecondsPerPercent; - - int remainingPercent = 100 - percent; - double etaSeconds = remainingPercent * smoothedSecondsPerPercent; - var newEta = TimeSpan.FromSeconds(Math.Max(0, etaSeconds)); - - // Only update if significant change - if (!lastReportedEta.HasValue || - Math.Abs(newEta.TotalSeconds - lastReportedEta.Value.TotalSeconds) / Math.Max(1, lastReportedEta.Value.TotalSeconds) > EtaChangeThreshold) - { - eta = newEta; - lastReportedEta = eta; - } - else - { - eta = lastReportedEta; // Keep previous ETA - } - - etaLastPercent = percent; - etaLastPercentTime = now; - } + eta.Update(totalUnits: 100, doneUnits: (long)Math.Round(percent)); + displayEta = eta.GetDisplayEta(); } else { - eta = lastReportedEta; + displayEta = eta.GetDisplayEta(); } - status = $"Installing · {percent}%"; + status = $"Installing · {percent:0.0}%"; break; + case PackageInstallProgressState.Installing: percent = 100; status = "Completing Installation..."; + displayEta = null; break; + case PackageInstallProgressState.Finished: percent = 100; status = ""; + displayEta = null; break; + default: - percent = 50; + percent = 100; + status = ""; + displayEta = null; break; } - // Throttle updates var updateNow = DateTime.UtcNow; bool shouldUpdate = (updateNow - lastProgressUpdate).TotalMilliseconds >= ThrottleMs - || percent != lastReportedPercent + || Math.Abs(percent - lastReportedPercent) >= 0.1f || args.State != PackageInstallProgressState.Uploading; if (shouldUpdate) { lastProgressUpdate = updateNow; lastReportedPercent = percent; - progressCallback?.Invoke(percent, eta); + + // ETA goes back via progress callback (label); status remains percent-only string for inner bar + progressCallback?.Invoke(percent, displayEta); if (status != null) statusCallback?.Invoke(status); } }; - // Install the package with progress await Task.Run(() => { packageManager.InstallPackage(path, installProgress); @@ -297,7 +264,6 @@ namespace AndroidSideloader { Logger.Log($"SideloadWithProgressAsync error: {ex.Message}", LogLevel.ERROR); - // Check for signature mismatch errors if (ex.Message.Contains("INSTALL_FAILED") || ex.Message.Contains("signatures do not match")) { @@ -318,7 +284,6 @@ namespace AndroidSideloader if (cancelClicked) return new ProcessOutput("", "Installation cancelled by user"); - // Perform reinstall statusCallback?.Invoke("Performing reinstall..."); try @@ -338,7 +303,7 @@ namespace AndroidSideloader { if (args.State == PackageInstallProgressState.Uploading) { - progressCallback?.Invoke((int)Math.Round(args.UploadProgress), null); + progressCallback?.Invoke((float)args.UploadProgress, null); } }; packageManager.InstallPackage(path, reinstallProgress); @@ -368,7 +333,7 @@ namespace AndroidSideloader // Copies OBB folder with real-time progress reporting using AdvancedSharpAdbClient public static async Task CopyOBBWithProgressAsync( string localPath, - Action progressCallback = null, // Now includes ETA + Action progressCallback = null, Action statusCallback = null, string gameName = "") { @@ -404,18 +369,11 @@ namespace AndroidSideloader // Throttle UI updates to prevent lag DateTime lastProgressUpdate = DateTime.MinValue; - int lastReportedPercent = -1; - const int ThrottleMs = 100; // Update UI at most every 100ms + float lastReportedPercent = -1; + const int ThrottleMs = 100; // Update UI every 100ms - // ETA tracking with smoothing - DateTime copyStart = DateTime.UtcNow; - int etaLastPercent = 0; - DateTime etaLastPercentTime = DateTime.UtcNow; - double smoothedSecondsPerPercent = 0; - TimeSpan? lastReportedEta = null; - TimeSpan? currentEta = null; - const double SmoothingAlpha = 0.15; // Lower = smoother, less responsive - const double EtaChangeThreshold = 0.10; // 10% change threshold + // Shared ETA engine (bytes-units) + var eta = new EtaEstimator(alpha: 0.10, reanchorThreshold: 0.20); statusCallback?.Invoke($"Copying: {folderName}"); @@ -437,71 +395,37 @@ namespace AndroidSideloader long fileSize = fileInfo.Length; long capturedTransferredBytes = transferredBytes; - // Progress handler for this file with throttling Action progressHandler = (args) => { long totalProgressBytes = capturedTransferredBytes + args.ReceivedBytesSize; - double overallPercent = totalBytes > 0 - ? (totalProgressBytes * 100.0) / totalBytes - : 0.0; + float overallPercent = totalBytes > 0 + ? (float)(totalProgressBytes * 100.0 / totalBytes) + : 0f; - int overallPercentInt = (int)Math.Round(overallPercent); - overallPercentInt = Math.Max(0, Math.Min(100, overallPercentInt)); + overallPercent = Math.Max(0, Math.Min(100, overallPercent)); - // Calculate ETA with smoothing - if (overallPercentInt > etaLastPercent && overallPercentInt < 100) + // Update ETA engine in bytes + if (totalBytes > 0 && totalProgressBytes > 0 && overallPercent < 100) { - var now = DateTime.UtcNow; - double secondsForThisChunk = (now - etaLastPercentTime).TotalSeconds; - int percentGained = overallPercentInt - etaLastPercent; - - if (percentGained > 0 && secondsForThisChunk > 0) - { - double secondsPerPercent = secondsForThisChunk / percentGained; - - // Exponential smoothing - if (smoothedSecondsPerPercent == 0) - smoothedSecondsPerPercent = secondsPerPercent; - else - smoothedSecondsPerPercent = SmoothingAlpha * secondsPerPercent + (1 - SmoothingAlpha) * smoothedSecondsPerPercent; - - int remainingPercent = 100 - overallPercentInt; - double etaSeconds = remainingPercent * smoothedSecondsPerPercent; - var newEta = TimeSpan.FromSeconds(Math.Max(0, etaSeconds)); - - // Only update if significant change - if (!lastReportedEta.HasValue || - Math.Abs(newEta.TotalSeconds - lastReportedEta.Value.TotalSeconds) / Math.Max(1, lastReportedEta.Value.TotalSeconds) > EtaChangeThreshold) - { - currentEta = newEta; - lastReportedEta = currentEta; - } - else - { - currentEta = lastReportedEta; - } - - etaLastPercent = overallPercentInt; - etaLastPercentTime = now; - } + eta.Update(totalUnits: totalBytes, doneUnits: totalProgressBytes); } - // Throttle updates + TimeSpan? displayEta = eta.GetDisplayEta(); + var now2 = DateTime.UtcNow; bool shouldUpdate = (now2 - lastProgressUpdate).TotalMilliseconds >= ThrottleMs - || overallPercentInt != lastReportedPercent; + || Math.Abs(overallPercent - lastReportedPercent) >= 0.1f; if (shouldUpdate) { lastProgressUpdate = now2; - lastReportedPercent = overallPercentInt; - progressCallback?.Invoke(overallPercentInt, currentEta); + lastReportedPercent = overallPercent; + progressCallback?.Invoke(overallPercent, displayEta); statusCallback?.Invoke(fileName); } }; - // Push the file with progress using (var stream = File.OpenRead(file)) { await Task.Run(() => @@ -786,4 +710,102 @@ namespace AndroidSideloader : new ProcessOutput("No OBB Folder found"); } } + + internal class EtaEstimator + { + private readonly double _alpha; // EWMA smoothing + private readonly double _reanchorThreshold; // % difference required to re-anchor + private readonly double _minSampleSeconds; // ignore too-short dt + + private DateTime _lastSampleTimeUtc; + private long _lastSampleDoneUnits; + private double _smoothedUnitsPerSecond; + + private TimeSpan? _etaAnchorValue; + private DateTime _etaAnchorTimeUtc; + + public EtaEstimator(double alpha, double reanchorThreshold, double minSampleSeconds = 0.15) + { + _alpha = alpha; + _reanchorThreshold = reanchorThreshold; + _minSampleSeconds = minSampleSeconds; + Reset(); + } + + public void Reset() + { + _lastSampleTimeUtc = DateTime.UtcNow; + _lastSampleDoneUnits = 0; + _smoothedUnitsPerSecond = 0; + _etaAnchorValue = null; + _etaAnchorTimeUtc = DateTime.UtcNow; + } + + // Updates internal rate estimate and re-anchors ETA + // totalUnits: total work units (e.g., 100 for percent, or totalBytes for bytes) + // doneUnits: completed work units so far (e.g., percent, or bytes transferred) + public void Update(long totalUnits, long doneUnits) + { + var now = DateTime.UtcNow; + if (totalUnits <= 0) return; + + doneUnits = Math.Max(0, Math.Min(totalUnits, doneUnits)); + + long remainingUnits = Math.Max(0, totalUnits - doneUnits); + + double dt = (now - _lastSampleTimeUtc).TotalSeconds; + long dUnits = doneUnits - _lastSampleDoneUnits; + + if (dt >= _minSampleSeconds && dUnits > 0) + { + double instUnitsPerSecond = dUnits / dt; + + if (_smoothedUnitsPerSecond <= 0) + _smoothedUnitsPerSecond = instUnitsPerSecond; + else + _smoothedUnitsPerSecond = _alpha * instUnitsPerSecond + (1 - _alpha) * _smoothedUnitsPerSecond; + + _lastSampleTimeUtc = now; + _lastSampleDoneUnits = doneUnits; + } + + if (_smoothedUnitsPerSecond > 1e-6 && remainingUnits > 0) + { + var newEta = TimeSpan.FromSeconds(remainingUnits / _smoothedUnitsPerSecond); + if (newEta < TimeSpan.Zero) newEta = TimeSpan.Zero; + + if (!_etaAnchorValue.HasValue) + { + _etaAnchorValue = newEta; + _etaAnchorTimeUtc = now; + } + else + { + // What countdown would currently show + var predictedNow = _etaAnchorValue.Value - (now - _etaAnchorTimeUtc); + if (predictedNow < TimeSpan.Zero) predictedNow = TimeSpan.Zero; + + double baseSeconds = Math.Max(1, predictedNow.TotalSeconds); + double diffRatio = Math.Abs(newEta.TotalSeconds - predictedNow.TotalSeconds) / baseSeconds; + + if (diffRatio > _reanchorThreshold) + { + _etaAnchorValue = newEta; + _etaAnchorTimeUtc = now; + } + } + } + } + + // Returns a countdown ETA for UI display + public TimeSpan? GetDisplayEta() + { + if (!_etaAnchorValue.HasValue) return null; + + var remaining = _etaAnchorValue.Value - (DateTime.UtcNow - _etaAnchorTimeUtc); + if (remaining < TimeSpan.Zero) remaining = TimeSpan.Zero; + + return TimeSpan.FromSeconds(Math.Ceiling(remaining.TotalSeconds)); + } + } } \ No newline at end of file diff --git a/MainForm.cs b/MainForm.cs index 23f29de..980a1e1 100755 --- a/MainForm.cs +++ b/MainForm.cs @@ -3052,7 +3052,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord gamesQueueList.RemoveAt(0); } - public void SetProgress(int progress) + public void SetProgress(float progress) { if (progressBar.InvokeRequired) { @@ -3264,7 +3264,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord speedLabel.Text = "Starting download..."; // Track the highest valid progress to prevent brief progress bar flashes during multi-file transfers - int highestValidPercent = 0; + float highestValidPercent = 0; // Download while (t1.IsAlive) @@ -3311,10 +3311,10 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord progressBar.IsIndeterminate = false; - int percent = 0; + float percent = 0; if (totalSize > 0) { - percent = Convert.ToInt32((downloadedSize / totalSize) * 100); + percent = (float)(downloadedSize / totalSize * 100); } // Clamp to 0-99 while download is in progress to prevent brief 100% flashes @@ -3339,7 +3339,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord "Downloading", (int)transfersComplete + 1, (int)fileCount, - percent, + (int)Math.Round(percent), time, downloadSpeed); } @@ -3441,9 +3441,9 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord this.Invoke(() => { progressBar.Value = percent; - UpdateProgressStatus("Extracting", percent: percent, eta: eta); + UpdateProgressStatus("Extracting", percent: (int)Math.Round(percent), eta: eta); - progressBar.StatusText = $"Extracting · {percent}%"; + progressBar.StatusText = $"Extracting · {percent:0.0}%"; }); }; @@ -3570,14 +3570,17 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord progressBar.IsIndeterminate = false; progressBar.Value = progress; } - UpdateProgressStatus("Installing APK", percent: progress, eta: eta); - progressBar.StatusText = $"Installing · {progress}%"; + UpdateProgressStatus("Installing", percent: (int)Math.Round(progress), eta: eta); + progressBar.StatusText = $"Installing · {progress:0.0}%"; }), status => this.Invoke(() => { if (!string.IsNullOrEmpty(status)) { - // "Completing Installation..." - speedLabel.Text = status; + if (status.Contains("Completing Installation")) + { + // "Completing Installation..." + speedLabel.Text = status; + } progressBar.StatusText = status; } }), @@ -3605,15 +3608,15 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord (progress, eta) => this.Invoke(() => { progressBar.Value = progress; - UpdateProgressStatus("Copying OBB", percent: progress, eta: eta); + UpdateProgressStatus("Copying OBB", percent: (int)Math.Round(progress), eta: eta); if (!string.IsNullOrEmpty(currentObbStatusBase)) { - progressBar.StatusText = $"{currentObbStatusBase} · {progress}%"; + progressBar.StatusText = $"{currentObbStatusBase} · {progress:0.0}%"; } else { - progressBar.StatusText = $"{progress}%"; + progressBar.StatusText = $"{progress:0.0}%"; } }), status => this.Invoke(() => @@ -4743,6 +4746,8 @@ let player; let pendingId = null; function onYouTubeIframeAPIReady() { player = new YT.Player('player', { + width: '100%', + height: '100%', playerVars: { autoplay: 0, mute: 1, diff --git a/ModernProgessBar.cs b/ModernProgessBar.cs index 1eb4b4a..bb8e350 100644 --- a/ModernProgessBar.cs +++ b/ModernProgessBar.cs @@ -13,9 +13,9 @@ namespace AndroidSideloader { #region Fields - private int _value; - private int _minimum; - private int _maximum = 100; + private float _value; + private float _minimum; + private float _maximum = 100f; private int _radius = 8; private bool _isIndeterminate; private string _statusText = string.Empty; @@ -66,7 +66,7 @@ namespace AndroidSideloader [Category("Progress")] [Description("The current value of the progress bar.")] - public int Value + public float Value { get => _value; set @@ -78,7 +78,7 @@ namespace AndroidSideloader [Category("Progress")] [Description("The minimum value of the progress bar.")] - public int Minimum + public float Minimum { get => _minimum; set @@ -91,7 +91,7 @@ namespace AndroidSideloader [Category("Progress")] [Description("The maximum value of the progress bar.")] - public int Maximum + public float Maximum { get => _maximum; set @@ -122,7 +122,7 @@ namespace AndroidSideloader set { // If there is no change, do nothing - if (_isIndeterminate == value) + if (_isIndeterminate == value) return; _isIndeterminate = value; @@ -205,7 +205,7 @@ namespace AndroidSideloader // Gets the progress as a percentage (0-100) public float ProgressPercent => - _maximum > _minimum ? (float)(_value - _minimum) / (_maximum - _minimum) * 100f : 0f; + _maximum > _minimum ? (_value - _minimum) / (_maximum - _minimum) * 100f : 0f; #endregion @@ -250,7 +250,7 @@ namespace AndroidSideloader private void DrawProgress(Graphics g, Rectangle outerRect) { float percent = (_maximum > _minimum) - ? (float)(_value - _minimum) / (_maximum - _minimum) + ? (_value - _minimum) / (_maximum - _minimum) : 0f; if (percent <= 0f) return; @@ -363,10 +363,11 @@ namespace AndroidSideloader if (!_isIndeterminate && _value > _minimum) { - string percentText = $"{(int)ProgressPercent}%"; + // Show one decimal place for sub-percent precision + string percentText = $"{ProgressPercent:0.0}%"; if (!string.IsNullOrEmpty(_operationType)) { - // E.g. "Downloading · 73%" + // E.g. "Downloading · 73.5%" return $"{_operationType} · {percentText}"; } return percentText; @@ -435,4 +436,4 @@ namespace AndroidSideloader #endregion } -} +} \ No newline at end of file diff --git a/Utilities/Zip.cs b/Utilities/Zip.cs index b5ab260..c0cb831 100644 --- a/Utilities/Zip.cs +++ b/Utilities/Zip.cs @@ -18,7 +18,7 @@ namespace AndroidSideloader.Utilities private static readonly SettingsManager settings = SettingsManager.Instance; // Progress callback: (percent, eta) - public static Action ExtractionProgressCallback { get; set; } + public static Action ExtractionProgressCallback { get; set; } public static Action ExtractionStatusCallback { get; set; } public static void ExtractFile(string sourceArchive, string destination) @@ -35,6 +35,7 @@ namespace AndroidSideloader.Utilities private static string extractionError = null; private static bool errorMessageShown = false; + private static void DoExtract(string args) { if (!File.Exists(Path.Combine(Environment.CurrentDirectory, "7z.exe")) || !File.Exists(Path.Combine(Environment.CurrentDirectory, "7z.dll"))) @@ -70,15 +71,20 @@ namespace AndroidSideloader.Utilities _ = Logger.Log($"Extract: 7z {string.Join(" ", args.Split(' ').Where(a => !a.StartsWith("-p")))}"); - // ETA tracking - DateTime extractStart = DateTime.UtcNow; - int etaLastPercent = 0; - DateTime etaLastPercentTime = DateTime.UtcNow; - double smoothedSecondsPerPercent = 0; - TimeSpan? lastReportedEta = null; - int lastReportedPercent = -1; - const double SmoothingAlpha = 0.15; - const double EtaChangeThreshold = 0.10; + // Throttle percent reports + float lastReportedPercent = -1; + + // ETA engine (percent units) + var etaEstimator = new EtaEstimator(alpha: 0.10, reanchorThreshold: 0.20, minSampleSeconds: 0.10); + + // Smooth progress (sub-percent) interpolation (because 7z -bsp1 is integer-only) + System.Threading.Timer smoothTimer = null; + int extractingFlag = 1; // 1 = extracting, 0 = stop + float smoothLastTickPercent = 0f; + DateTime smoothLastTickTime = DateTime.UtcNow; + float smoothLastReported = -1f; + const int SmoothIntervalMs = 80; // ~12.5 updates/sec + const float SmoothReportDelta = 0.10f; // report only if change >= 0.10% using (Process x = new Process()) { @@ -86,59 +92,67 @@ namespace AndroidSideloader.Utilities if (MainForm.isInDownloadExtract && x != null) { + // Smooth sub-percent UI, while keeping ETA ticking + smoothTimer = new System.Threading.Timer(_ => + { + if (System.Threading.Volatile.Read(ref extractingFlag) == 0) return; + if (smoothLastTickPercent <= 0) return; // need at least one 7z tick + + // Use current ETA to approximate seconds-per-percent + TimeSpan? displayEta = etaEstimator.GetDisplayEta(); + if (!displayEta.HasValue) return; // Skip until ETA exists + + var now = DateTime.UtcNow; + var elapsed = (now - smoothLastTickTime).TotalSeconds; + + // Approx seconds-per-percent from remaining ETA / remaining percent + double remainingPercent = Math.Max(1.0, 100.0 - smoothLastTickPercent); + double spp = Math.Max(0.05, displayEta.Value.TotalSeconds / remainingPercent); + + float candidate = smoothLastTickPercent + (float)(elapsed / spp); + + // Clamp + float floorTick = (float)Math.Floor(smoothLastTickPercent); + float ceiling = Math.Min(99.99f, floorTick + 0.999f); + + if (candidate > ceiling) candidate = ceiling; + if (candidate < smoothLastTickPercent) candidate = smoothLastTickPercent; + + if (smoothLastReported >= 0 && Math.Abs(candidate - smoothLastReported) < SmoothReportDelta) return; + smoothLastReported = candidate; + + try + { + MainForm mainForm = (MainForm)Application.OpenForms[0]; + if (mainForm != null && !mainForm.IsDisposed) + { + mainForm.BeginInvoke((Action)(() => mainForm.SetProgress(candidate))); + } + } + catch { } + + // ETA countdown ticks even if 7z percent is unchanged + ExtractionProgressCallback?.Invoke(candidate, etaEstimator.GetDisplayEta()); + + }, null, SmoothIntervalMs, SmoothIntervalMs); + x.OutputDataReceived += (sender, e) => { if (e.Data != null) { - // Parse 7-Zip progress output (e.g., " 45% - filename") var match = Regex.Match(e.Data, @"^\s*(\d+)%"); - if (match.Success && int.TryParse(match.Groups[1].Value, out int percent)) + if (match.Success && float.TryParse(match.Groups[1].Value, out float percent)) { - TimeSpan? eta = null; + // Update ETA from integer percent + if (percent <= 0.0f) etaEstimator.Reset(); + else if (percent < 100.0f) etaEstimator.Update(totalUnits: 100, doneUnits: (long)Math.Round(percent)); - // Calculate ETA - if (percent > etaLastPercent && percent < 100) - { - var now = DateTime.UtcNow; - double secondsForThisChunk = (now - etaLastPercentTime).TotalSeconds; - int percentGained = percent - etaLastPercent; + // Reset smoothing baseline on each integer tick + smoothLastTickPercent = percent; + smoothLastTickTime = DateTime.UtcNow; + smoothLastReported = percent; - if (percentGained > 0 && secondsForThisChunk > 0) - { - double secondsPerPercent = secondsForThisChunk / percentGained; - - if (smoothedSecondsPerPercent == 0) - smoothedSecondsPerPercent = secondsPerPercent; - else - smoothedSecondsPerPercent = SmoothingAlpha * secondsPerPercent + (1 - SmoothingAlpha) * smoothedSecondsPerPercent; - - int remainingPercent = 100 - percent; - double etaSeconds = remainingPercent * smoothedSecondsPerPercent; - var newEta = TimeSpan.FromSeconds(Math.Max(0, etaSeconds)); - - // Only update if significant change - if (!lastReportedEta.HasValue || - Math.Abs(newEta.TotalSeconds - lastReportedEta.Value.TotalSeconds) / Math.Max(1, lastReportedEta.Value.TotalSeconds) > EtaChangeThreshold) - { - eta = newEta; - lastReportedEta = eta; - } - else - { - eta = lastReportedEta; - } - - etaLastPercent = percent; - etaLastPercentTime = now; - } - } - else - { - eta = lastReportedEta; - } - - // Only report if percent changed - if (percent != lastReportedPercent) + if (Math.Abs(percent - lastReportedPercent) >= 0.1f) { lastReportedPercent = percent; @@ -148,7 +162,7 @@ namespace AndroidSideloader.Utilities mainForm.Invoke((Action)(() => mainForm.SetProgress(percent))); } - ExtractionProgressCallback?.Invoke(percent, eta); + ExtractionProgressCallback?.Invoke(percent, etaEstimator.GetDisplayEta()); } } } @@ -183,6 +197,11 @@ namespace AndroidSideloader.Utilities x.BeginErrorReadLine(); x.WaitForExit(); + // Stop smoother + System.Threading.Interlocked.Exchange(ref extractingFlag, 0); + smoothTimer?.Dispose(); + smoothTimer = null; + // Clear callbacks ExtractionProgressCallback?.Invoke(100, null); ExtractionStatusCallback?.Invoke(""); @@ -198,4 +217,4 @@ namespace AndroidSideloader.Utilities } } } -} +} \ No newline at end of file