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
811 lines
34 KiB
C#
811 lines
34 KiB
C#
using AdvancedSharpAdbClient;
|
|
using AdvancedSharpAdbClient.DeviceCommands;
|
|
using AdvancedSharpAdbClient.Models;
|
|
using AdvancedSharpAdbClient.Receivers;
|
|
using AndroidSideloader.Utilities;
|
|
using JR.Utils.GUI.Forms;
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
|
|
namespace AndroidSideloader
|
|
{
|
|
internal class ADB
|
|
{
|
|
private static readonly SettingsManager settings = SettingsManager.Instance;
|
|
public static string adbFolderPath = Path.Combine(Environment.CurrentDirectory, "platform-tools");
|
|
public static string adbFilePath = Path.Combine(adbFolderPath, "adb.exe");
|
|
public static string DeviceID = "";
|
|
public static string package = "";
|
|
public static bool wirelessadbON;
|
|
|
|
// AdbClient for direct protocol communication
|
|
private static AdbClient _adbClient;
|
|
private static DeviceData _currentDevice;
|
|
|
|
// Gets or initializes the AdbClient instance
|
|
private static AdbClient GetAdbClient()
|
|
{
|
|
if (_adbClient == null)
|
|
{
|
|
// Ensure ADB server is started
|
|
if (!AdbServer.Instance.GetStatus().IsRunning)
|
|
{
|
|
var server = new AdbServer();
|
|
var result = server.StartServer(adbFilePath, false);
|
|
Logger.Log($"ADB server start result: {result}");
|
|
}
|
|
|
|
_adbClient = new AdbClient();
|
|
}
|
|
return _adbClient;
|
|
}
|
|
|
|
// Gets the current device for AdbClient operations
|
|
private static DeviceData GetCurrentDevice()
|
|
{
|
|
var client = GetAdbClient();
|
|
var devices = client.GetDevices();
|
|
|
|
if (devices == null || !devices.Any())
|
|
{
|
|
Logger.Log("No devices found via AdbClient", LogLevel.WARNING);
|
|
return default;
|
|
}
|
|
|
|
// If DeviceID is set, find that specific device
|
|
if (!string.IsNullOrEmpty(DeviceID) && DeviceID.Length > 1)
|
|
{
|
|
var device = devices.FirstOrDefault(d => d.Serial == DeviceID || d.Serial.StartsWith(DeviceID));
|
|
if (device.Serial != null)
|
|
{
|
|
_currentDevice = device;
|
|
return device;
|
|
}
|
|
}
|
|
|
|
// Otherwise return the first available device
|
|
_currentDevice = devices.First();
|
|
return _currentDevice;
|
|
}
|
|
|
|
public static ProcessOutput RunAdbCommandToString(string command)
|
|
{
|
|
command = command.Replace("adb", "");
|
|
|
|
settings.ADBFolder = adbFolderPath;
|
|
settings.ADBPath = adbFilePath;
|
|
settings.Save();
|
|
|
|
if (DeviceID.Length > 1)
|
|
{
|
|
command = $" -s {DeviceID} {command}";
|
|
}
|
|
|
|
if (!command.Contains("dumpsys") && !command.Contains("shell pm list packages") && !command.Contains("KEYCODE_WAKEUP"))
|
|
{
|
|
string logcmd = command;
|
|
if (logcmd.Contains(Environment.CurrentDirectory))
|
|
{
|
|
logcmd = logcmd.Replace($"{Environment.CurrentDirectory}", $"CurrentDirectory");
|
|
}
|
|
_ = Logger.Log($"Running command: {logcmd}");
|
|
}
|
|
|
|
using (Process adb = new Process())
|
|
{
|
|
adb.StartInfo.FileName = adbFilePath;
|
|
adb.StartInfo.Arguments = command;
|
|
adb.StartInfo.RedirectStandardError = true;
|
|
adb.StartInfo.RedirectStandardOutput = true;
|
|
adb.StartInfo.CreateNoWindow = true;
|
|
adb.StartInfo.UseShellExecute = false;
|
|
adb.StartInfo.WorkingDirectory = adbFolderPath;
|
|
_ = adb.Start();
|
|
|
|
string output = "";
|
|
string error = "";
|
|
|
|
try
|
|
{
|
|
output = adb.StandardOutput.ReadToEnd();
|
|
error = adb.StandardError.ReadToEnd();
|
|
}
|
|
catch { }
|
|
|
|
if (command.Contains("connect"))
|
|
{
|
|
bool graceful = adb.WaitForExit(3000);
|
|
if (!graceful)
|
|
{
|
|
adb.Kill();
|
|
adb.WaitForExit();
|
|
}
|
|
}
|
|
|
|
if (error.Contains("ADB_VENDOR_KEYS") && !settings.AdbDebugWarned)
|
|
{
|
|
ADBDebugWarning();
|
|
}
|
|
if (error.Contains("not enough storage space"))
|
|
{
|
|
_ = FlexibleMessageBox.Show(Program.form, "There is not enough room on your device to install this package. Please clear AT LEAST 2x the amount of the app you are trying to install.");
|
|
}
|
|
if (!output.Contains("version") && !output.Contains("KEYCODE_WAKEUP") && !output.Contains("Filesystem") && !output.Contains("package:") && !output.Equals(null))
|
|
{
|
|
_ = Logger.Log(output);
|
|
}
|
|
|
|
_ = Logger.Log(error, LogLevel.ERROR);
|
|
return new ProcessOutput(output, error);
|
|
}
|
|
}
|
|
|
|
// Executes a shell command on the device.
|
|
private static void ExecuteShellCommand(AdbClient client, DeviceData device, string command)
|
|
{
|
|
var receiver = new ConsoleOutputReceiver();
|
|
client.ExecuteRemoteCommand(command, device, receiver);
|
|
}
|
|
|
|
// Copies and installs an APK with real-time progress reporting using AdvancedSharpAdbClient
|
|
public static async Task<ProcessOutput> SideloadWithProgressAsync(
|
|
string path,
|
|
Action<float, TimeSpan?> progressCallback = null,
|
|
Action<string> statusCallback = null,
|
|
string packagename = "",
|
|
string gameName = "")
|
|
{
|
|
statusCallback?.Invoke("Installing APK...");
|
|
progressCallback?.Invoke(0, null);
|
|
|
|
try
|
|
{
|
|
var device = GetCurrentDevice();
|
|
if (device.Serial == null)
|
|
{
|
|
return new ProcessOutput("", "No device connected");
|
|
}
|
|
|
|
var client = GetAdbClient();
|
|
var packageManager = new PackageManager(client, device);
|
|
|
|
statusCallback?.Invoke("Installing APK...");
|
|
|
|
// Throttle UI updates to prevent lag
|
|
DateTime lastProgressUpdate = DateTime.MinValue;
|
|
float lastReportedPercent = -1;
|
|
const int ThrottleMs = 100; // Update UI every 100ms
|
|
|
|
// Shared ETA engine (percent-units)
|
|
var eta = new EtaEstimator(alpha: 0.05, reanchorThreshold: 0.20);
|
|
|
|
// Create install progress handler
|
|
Action<InstallProgressEventArgs> installProgress = (args) =>
|
|
{
|
|
float percent = 0;
|
|
string status = null;
|
|
TimeSpan? displayEta = null;
|
|
|
|
switch (args.State)
|
|
{
|
|
case PackageInstallProgressState.Preparing:
|
|
percent = 0;
|
|
status = "Preparing...";
|
|
eta.Reset();
|
|
break;
|
|
|
|
case PackageInstallProgressState.Uploading:
|
|
percent = (float)args.UploadProgress;
|
|
|
|
// Update ETA engine using percent as units (0..100)
|
|
if (percent > 0 && percent < 100)
|
|
{
|
|
eta.Update(totalUnits: 100, doneUnits: (long)Math.Round(percent));
|
|
displayEta = eta.GetDisplayEta();
|
|
}
|
|
else
|
|
{
|
|
displayEta = eta.GetDisplayEta();
|
|
}
|
|
|
|
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 = 100;
|
|
status = "";
|
|
displayEta = null;
|
|
break;
|
|
}
|
|
|
|
var updateNow = DateTime.UtcNow;
|
|
bool shouldUpdate = (updateNow - lastProgressUpdate).TotalMilliseconds >= ThrottleMs
|
|
|| Math.Abs(percent - lastReportedPercent) >= 0.1f
|
|
|| args.State != PackageInstallProgressState.Uploading;
|
|
|
|
if (shouldUpdate)
|
|
{
|
|
lastProgressUpdate = updateNow;
|
|
lastReportedPercent = percent;
|
|
|
|
// 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);
|
|
}
|
|
};
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
packageManager.InstallPackage(path, installProgress);
|
|
});
|
|
|
|
progressCallback?.Invoke(100, null);
|
|
statusCallback?.Invoke("");
|
|
|
|
return new ProcessOutput($"{gameName}: Success\n");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Log($"SideloadWithProgressAsync error: {ex.Message}", LogLevel.ERROR);
|
|
|
|
if (ex.Message.Contains("INSTALL_FAILED") ||
|
|
ex.Message.Contains("signatures do not match"))
|
|
{
|
|
bool cancelClicked = false;
|
|
|
|
if (!settings.AutoReinstall)
|
|
{
|
|
Program.form.Invoke(() =>
|
|
{
|
|
DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form,
|
|
"In place upgrade has failed. Rookie can attempt to backup your save data and reinstall the game automatically, however some games do not store their saves in an accessible location (less than 5%). Continue with reinstall?",
|
|
"In place upgrade failed.", MessageBoxButtons.OKCancel);
|
|
if (dialogResult1 == DialogResult.Cancel)
|
|
cancelClicked = true;
|
|
});
|
|
}
|
|
|
|
if (cancelClicked)
|
|
return new ProcessOutput("", "Installation cancelled by user");
|
|
|
|
statusCallback?.Invoke("Performing reinstall...");
|
|
|
|
try
|
|
{
|
|
var device = GetCurrentDevice();
|
|
var client = GetAdbClient();
|
|
var packageManager = new PackageManager(client, device);
|
|
|
|
statusCallback?.Invoke("Backing up save data...");
|
|
_ = RunAdbCommandToString($"pull \"/sdcard/Android/data/{MainForm.CurrPCKG}\" \"{Environment.CurrentDirectory}\"");
|
|
|
|
statusCallback?.Invoke("Uninstalling old version...");
|
|
packageManager.UninstallPackage(packagename);
|
|
|
|
statusCallback?.Invoke("Reinstalling game...");
|
|
Action<InstallProgressEventArgs> reinstallProgress = (args) =>
|
|
{
|
|
if (args.State == PackageInstallProgressState.Uploading)
|
|
{
|
|
progressCallback?.Invoke((float)args.UploadProgress, null);
|
|
}
|
|
};
|
|
packageManager.InstallPackage(path, reinstallProgress);
|
|
|
|
statusCallback?.Invoke("Restoring save data...");
|
|
_ = RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{MainForm.CurrPCKG}\" /sdcard/Android/data/");
|
|
|
|
string directoryToDelete = Path.Combine(Environment.CurrentDirectory, MainForm.CurrPCKG);
|
|
if (Directory.Exists(directoryToDelete) && directoryToDelete != Environment.CurrentDirectory)
|
|
{
|
|
Directory.Delete(directoryToDelete, true);
|
|
}
|
|
|
|
progressCallback?.Invoke(100, null);
|
|
return new ProcessOutput($"{gameName}: Reinstall: Success\n", "");
|
|
}
|
|
catch (Exception reinstallEx)
|
|
{
|
|
return new ProcessOutput($"{gameName}: Reinstall: Failed: {reinstallEx.Message}\n");
|
|
}
|
|
}
|
|
|
|
return new ProcessOutput("", ex.Message);
|
|
}
|
|
}
|
|
|
|
// Copies OBB folder with real-time progress reporting using AdvancedSharpAdbClient
|
|
public static async Task<ProcessOutput> CopyOBBWithProgressAsync(
|
|
string localPath,
|
|
Action<float, TimeSpan?> progressCallback = null,
|
|
Action<string> statusCallback = null,
|
|
string gameName = "")
|
|
{
|
|
string folderName = Path.GetFileName(localPath);
|
|
|
|
if (!folderName.Contains("."))
|
|
{
|
|
return new ProcessOutput("No OBB Folder found");
|
|
}
|
|
|
|
try
|
|
{
|
|
var device = GetCurrentDevice();
|
|
if (device.Serial == null)
|
|
{
|
|
return new ProcessOutput("", "No device connected");
|
|
}
|
|
|
|
var client = GetAdbClient();
|
|
string remotePath = $"/sdcard/Android/obb/{folderName}";
|
|
|
|
statusCallback?.Invoke($"Preparing: {folderName}");
|
|
progressCallback?.Invoke(0, null);
|
|
|
|
// Delete existing OBB folder and create new one
|
|
ExecuteShellCommand(client, device, $"rm -rf \"{remotePath}\"");
|
|
ExecuteShellCommand(client, device, $"mkdir -p \"{remotePath}\"");
|
|
|
|
// Get all files to push and calculate total size
|
|
var files = Directory.GetFiles(localPath, "*", SearchOption.AllDirectories);
|
|
long totalBytes = files.Sum(f => new FileInfo(f).Length);
|
|
long transferredBytes = 0;
|
|
|
|
// Throttle UI updates to prevent lag
|
|
DateTime lastProgressUpdate = DateTime.MinValue;
|
|
float lastReportedPercent = -1;
|
|
const int ThrottleMs = 100; // Update UI every 100ms
|
|
|
|
// Shared ETA engine (bytes-units)
|
|
var eta = new EtaEstimator(alpha: 0.10, reanchorThreshold: 0.20);
|
|
|
|
statusCallback?.Invoke($"Copying: {folderName}");
|
|
|
|
using (var syncService = new SyncService(client, device))
|
|
{
|
|
foreach (var file in files)
|
|
{
|
|
string relativePath = file.Substring(localPath.Length)
|
|
.TrimStart('\\', '/')
|
|
.Replace('\\', '/');
|
|
string remoteFilePath = $"{remotePath}/{relativePath}";
|
|
string fileName = Path.GetFileName(file);
|
|
|
|
// Ensure remote directory exists
|
|
string remoteDir = remoteFilePath.Substring(0, remoteFilePath.LastIndexOf('/'));
|
|
ExecuteShellCommand(client, device, $"mkdir -p \"{remoteDir}\"");
|
|
|
|
var fileInfo = new FileInfo(file);
|
|
long fileSize = fileInfo.Length;
|
|
long capturedTransferredBytes = transferredBytes;
|
|
|
|
Action<SyncProgressChangedEventArgs> progressHandler = (args) =>
|
|
{
|
|
long totalProgressBytes = capturedTransferredBytes + args.ReceivedBytesSize;
|
|
|
|
float overallPercent = totalBytes > 0
|
|
? (float)(totalProgressBytes * 100.0 / totalBytes)
|
|
: 0f;
|
|
|
|
overallPercent = Math.Max(0, Math.Min(100, overallPercent));
|
|
|
|
// Update ETA engine in bytes
|
|
if (totalBytes > 0 && totalProgressBytes > 0 && overallPercent < 100)
|
|
{
|
|
eta.Update(totalUnits: totalBytes, doneUnits: totalProgressBytes);
|
|
}
|
|
|
|
TimeSpan? displayEta = eta.GetDisplayEta();
|
|
|
|
var now2 = DateTime.UtcNow;
|
|
bool shouldUpdate = (now2 - lastProgressUpdate).TotalMilliseconds >= ThrottleMs
|
|
|| Math.Abs(overallPercent - lastReportedPercent) >= 0.1f;
|
|
|
|
if (shouldUpdate)
|
|
{
|
|
lastProgressUpdate = now2;
|
|
lastReportedPercent = overallPercent;
|
|
progressCallback?.Invoke(overallPercent, displayEta);
|
|
statusCallback?.Invoke(fileName);
|
|
}
|
|
};
|
|
|
|
using (var stream = File.OpenRead(file))
|
|
{
|
|
await Task.Run(() =>
|
|
{
|
|
syncService.Push(
|
|
stream,
|
|
remoteFilePath,
|
|
UnixFileStatus.DefaultFileMode,
|
|
DateTime.Now,
|
|
progressHandler,
|
|
false);
|
|
});
|
|
}
|
|
|
|
transferredBytes += fileSize;
|
|
}
|
|
}
|
|
|
|
progressCallback?.Invoke(100, null);
|
|
statusCallback?.Invoke("");
|
|
|
|
return new ProcessOutput($"{gameName}: OBB transfer: Success\n", "");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Log($"CopyOBBWithProgressAsync error: {ex.Message}", LogLevel.ERROR);
|
|
return new ProcessOutput("", $"{gameName}: OBB transfer: Failed: {ex.Message}\n");
|
|
}
|
|
}
|
|
|
|
public static ProcessOutput RunAdbCommandToStringWOADB(string result, string path)
|
|
{
|
|
string command = result;
|
|
string logcmd = command;
|
|
if (logcmd.Contains(Environment.CurrentDirectory))
|
|
{
|
|
logcmd = logcmd.Replace($"{Environment.CurrentDirectory}", $"CurrentDirectory");
|
|
}
|
|
|
|
_ = Logger.Log($"Running command: {logcmd}");
|
|
|
|
using (var adb = new Process())
|
|
{
|
|
adb.StartInfo.FileName = "cmd.exe";
|
|
adb.StartInfo.RedirectStandardError = true;
|
|
adb.StartInfo.RedirectStandardInput = true;
|
|
adb.StartInfo.RedirectStandardOutput = true;
|
|
adb.StartInfo.CreateNoWindow = true;
|
|
adb.StartInfo.UseShellExecute = false;
|
|
adb.StartInfo.WorkingDirectory = Path.GetDirectoryName(path);
|
|
_ = adb.Start();
|
|
adb.StandardInput.WriteLine(command);
|
|
adb.StandardInput.Flush();
|
|
adb.StandardInput.Close();
|
|
|
|
string output = "";
|
|
string error = "";
|
|
|
|
try
|
|
{
|
|
output += adb.StandardOutput.ReadToEnd();
|
|
error += adb.StandardError.ReadToEnd();
|
|
}
|
|
catch { }
|
|
|
|
if (command.Contains("connect"))
|
|
{
|
|
bool graceful = adb.WaitForExit(3000);
|
|
if (!graceful)
|
|
{
|
|
adb.Kill();
|
|
adb.WaitForExit();
|
|
}
|
|
}
|
|
|
|
if (error.Contains("ADB_VENDOR_KEYS") && settings.AdbDebugWarned)
|
|
{
|
|
ADBDebugWarning();
|
|
}
|
|
|
|
_ = Logger.Log(output);
|
|
_ = Logger.Log(error, LogLevel.ERROR);
|
|
return new ProcessOutput(output, error);
|
|
}
|
|
}
|
|
|
|
public static ProcessOutput RunCommandToString(string result, string path = "")
|
|
{
|
|
string command = result;
|
|
string logcmd = command;
|
|
if (logcmd.Contains(Environment.CurrentDirectory))
|
|
{
|
|
logcmd = logcmd.Replace($"{Environment.CurrentDirectory}", $"CurrentDirectory");
|
|
}
|
|
|
|
Logger.Log($"Running command: {logcmd}");
|
|
|
|
try
|
|
{
|
|
using (var proc = new Process())
|
|
{
|
|
proc.StartInfo.FileName = $@"{Path.GetPathRoot(Environment.SystemDirectory)}\Windows\System32\cmd.exe";
|
|
proc.StartInfo.Arguments = command;
|
|
proc.StartInfo.RedirectStandardError = true;
|
|
proc.StartInfo.RedirectStandardInput = true;
|
|
proc.StartInfo.RedirectStandardOutput = true;
|
|
proc.StartInfo.CreateNoWindow = true;
|
|
proc.StartInfo.UseShellExecute = false;
|
|
proc.StartInfo.WorkingDirectory = Path.GetDirectoryName(path);
|
|
|
|
proc.Start();
|
|
proc.StandardInput.WriteLine(command);
|
|
proc.StandardInput.Flush();
|
|
proc.StandardInput.Close();
|
|
|
|
string output = proc.StandardOutput.ReadToEnd();
|
|
string error = proc.StandardError.ReadToEnd();
|
|
|
|
if (command.Contains("connect"))
|
|
{
|
|
bool graceful = proc.WaitForExit(3000);
|
|
if (!graceful)
|
|
{
|
|
proc.Kill();
|
|
proc.WaitForExit();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
proc.WaitForExit();
|
|
}
|
|
|
|
if (error.Contains("ADB_VENDOR_KEYS") && settings.AdbDebugWarned)
|
|
{
|
|
ADBDebugWarning();
|
|
}
|
|
|
|
Logger.Log(output);
|
|
Logger.Log(error, LogLevel.ERROR);
|
|
|
|
return new ProcessOutput(output, error);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Log($"Error in RunCommandToString: {ex.Message}", LogLevel.ERROR);
|
|
return new ProcessOutput("", $"Exception occurred: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public static void ADBDebugWarning()
|
|
{
|
|
Program.form.Invoke(() =>
|
|
{
|
|
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form,
|
|
"On your headset, click on the Notifications Bell, and then select the USB Detected notification to enable Connections.",
|
|
"ADB Debugging not enabled.", MessageBoxButtons.OKCancel);
|
|
if (dialogResult == DialogResult.Cancel)
|
|
{
|
|
settings.Save();
|
|
}
|
|
});
|
|
}
|
|
|
|
public static ProcessOutput UninstallPackage(string package)
|
|
{
|
|
ProcessOutput output = new ProcessOutput("", "");
|
|
output += RunAdbCommandToString($"shell pm uninstall {package}");
|
|
|
|
// Prefix the output with the simple game name
|
|
string label = Sideloader.gameNameToSimpleName(Sideloader.PackageNametoGameName(package));
|
|
|
|
if (!string.IsNullOrEmpty(output.Output))
|
|
{
|
|
output.Output = $"{label}: {output.Output}";
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(output.Error))
|
|
{
|
|
output.Error = $"{label}: {output.Error}";
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
public static string GetAvailableSpace()
|
|
{
|
|
long totalSize = 0;
|
|
long usedSize = 0;
|
|
long freeSize = 0;
|
|
|
|
string[] output = RunAdbCommandToString("shell df").Output.Split('\n');
|
|
|
|
foreach (string currLine in output)
|
|
{
|
|
if (currLine.StartsWith("/dev/fuse") || currLine.StartsWith("/data/media"))
|
|
{
|
|
string[] foo = currLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (foo.Length >= 4)
|
|
{
|
|
totalSize = long.Parse(foo[1]) / 1000;
|
|
usedSize = long.Parse(foo[2]) / 1000;
|
|
freeSize = long.Parse(foo[3]) / 1000;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $"Total space: {string.Format("{0:0.00}", (double)totalSize / 1000)}GB\nUsed space: {string.Format("{0:0.00}", (double)usedSize / 1000)}GB\nFree space: {string.Format("{0:0.00}", (double)freeSize / 1000)}GB";
|
|
}
|
|
|
|
public static ProcessOutput Sideload(string path, string packagename = "")
|
|
{
|
|
ProcessOutput ret = new ProcessOutput();
|
|
ret += RunAdbCommandToString($"install -g \"{path}\"");
|
|
string out2 = ret.Output + ret.Error;
|
|
|
|
if (out2.Contains("failed"))
|
|
{
|
|
_ = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
|
_ = Logger.Log(out2);
|
|
|
|
if (out2.Contains("offline") && !settings.NodeviceMode)
|
|
{
|
|
DialogResult dialogResult2 = FlexibleMessageBox.Show(Program.form, "Device is offline. Press Yes to reconnect, or if you don't wish to connect and just want to download the game (requires unchecking \"Delete games after install\" from settings menu) then press No.", "Device offline.", MessageBoxButtons.YesNoCancel);
|
|
}
|
|
|
|
if (out2.Contains($"signatures do not match previously") || out2.Contains("INSTALL_FAILED_VERSION_DOWNGRADE") || out2.Contains("signatures do not match") || out2.Contains("failed to install"))
|
|
{
|
|
ret.Error = string.Empty;
|
|
ret.Output = string.Empty;
|
|
|
|
bool cancelClicked = false;
|
|
|
|
if (!settings.AutoReinstall)
|
|
{
|
|
Program.form.Invoke((MethodInvoker)(() =>
|
|
{
|
|
DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, "In place upgrade has failed. Rookie can attempt to backup your save data and reinstall the game automatically, however some games do not store their saves in an accessible location (less than 5%). Continue with reinstall?", "In place upgrade failed.", MessageBoxButtons.OKCancel);
|
|
if (dialogResult1 == DialogResult.Cancel)
|
|
cancelClicked = true;
|
|
}));
|
|
}
|
|
|
|
if (cancelClicked)
|
|
return ret;
|
|
|
|
Program.form.changeTitle("Performing reinstall, please wait...");
|
|
_ = RunAdbCommandToString("kill-server");
|
|
_ = RunAdbCommandToString("devices");
|
|
_ = RunAdbCommandToString($"pull \"/sdcard/Android/data/{MainForm.CurrPCKG}\" \"{Environment.CurrentDirectory}\"");
|
|
Program.form.changeTitle("Uninstalling game...");
|
|
_ = Sideloader.UninstallGame(MainForm.CurrPCKG);
|
|
Program.form.changeTitle("Reinstalling game...");
|
|
ret += RunAdbCommandToString($"install -g \"{path}\"");
|
|
_ = RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{MainForm.CurrPCKG}\" /sdcard/Android/data/");
|
|
|
|
string directoryToDelete = Path.Combine(Environment.CurrentDirectory, MainForm.CurrPCKG);
|
|
if (Directory.Exists(directoryToDelete))
|
|
{
|
|
if (directoryToDelete != Environment.CurrentDirectory)
|
|
{
|
|
Directory.Delete(directoryToDelete, true);
|
|
}
|
|
}
|
|
|
|
Program.form.changeTitle("");
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
Program.form.changeTitle("");
|
|
return ret;
|
|
}
|
|
|
|
public static ProcessOutput CopyOBB(string path)
|
|
{
|
|
string folder = Path.GetFileName(path);
|
|
string lastFolder = Path.GetFileName(path);
|
|
return folder.Contains(".")
|
|
? RunAdbCommandToString($"shell rm -rf \"/sdcard/Android/obb/{lastFolder}\" && mkdir \"/sdcard/Android/obb/{lastFolder}\"") + RunAdbCommandToString($"push \"{path}\" \"/sdcard/Android/obb\"")
|
|
: 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));
|
|
}
|
|
}
|
|
} |