Refactored progress bar to show fractional percentages and improved ETA smoothing

Progress values and callbacks (labels, progressbar) now use float instead of int to reflect fractional percentages for a better / more responsive user experience. Improved ETA display for APK install, OBB copy and ZIP extraction operations by introducing a reusable EtaEstimator class for smoother and more accurate ETA calculations
This commit is contained in:
jp64k
2025-12-17 02:20:40 +01:00
parent 2f843bc458
commit c5b151471a
4 changed files with 252 additions and 205 deletions

266
ADB.cs
View File

@@ -154,7 +154,7 @@ namespace AndroidSideloader
// Copies and installs an APK with real-time progress reporting using AdvancedSharpAdbClient
public static async Task<ProcessOutput> SideloadWithProgressAsync(
string path,
Action<int, TimeSpan?> progressCallback = null, // Now includes ETA
Action<float, TimeSpan?> progressCallback = null,
Action<string> 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<InstallProgressEventArgs> 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<ProcessOutput> CopyOBBWithProgressAsync(
string localPath,
Action<int, TimeSpan?> progressCallback = null, // Now includes ETA
Action<float, TimeSpan?> progressCallback = null,
Action<string> 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<SyncProgressChangedEventArgs> 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));
}
}
}

View File

@@ -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,

View File

@@ -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
@@ -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;

View File

@@ -18,7 +18,7 @@ namespace AndroidSideloader.Utilities
private static readonly SettingsManager settings = SettingsManager.Instance;
// Progress callback: (percent, eta)
public static Action<int, TimeSpan?> ExtractionProgressCallback { get; set; }
public static Action<float, TimeSpan?> ExtractionProgressCallback { get; set; }
public static Action<string> 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("");