diff --git a/MetaforceInstaller.Cli/MetaforceInstaller.Cli.csproj b/MetaforceInstaller.Cli/MetaforceInstaller.Cli.csproj index 79e2376..dee32c2 100644 --- a/MetaforceInstaller.Cli/MetaforceInstaller.Cli.csproj +++ b/MetaforceInstaller.Cli/MetaforceInstaller.Cli.csproj @@ -7,14 +7,16 @@ enable + + + + - + - - - + diff --git a/MetaforceInstaller.Cli/Program.cs b/MetaforceInstaller.Cli/Program.cs index fa8c389..bf8bf51 100644 --- a/MetaforceInstaller.Cli/Program.cs +++ b/MetaforceInstaller.Cli/Program.cs @@ -1,57 +1,54 @@ -using System.Reflection; -using AdvancedSharpAdbClient; -using AdvancedSharpAdbClient.DeviceCommands; -using AdvancedSharpAdbClient.Models; +using MetaforceInstaller.Cli.Utils; +using MetaforceInstaller.Core.Services; namespace MetaforceInstaller.Cli; -class Program +static class Program { - // 1. Получить имя апк и зипки, если не предоставлены - забить дефолтными значениями - // 2. Распаковать в временную директорию adb (готово) - // 3. Установить апк - // 4. Получить имя пакета - // 5. Сформировать строку пути для контента - // 6. Копировать зип по сформированному пути - - static AdbClient adbClient; - static DeviceData deviceData; - - static void Main(string[] args) + static async Task Main(string[] args) { try { - var (apkPath, zipPath, outputPath) = ParseArguments(args); + var installationRequest = ArgumentParser.ParseArguments(args); - if (string.IsNullOrEmpty(apkPath) || string.IsNullOrEmpty(zipPath) || string.IsNullOrEmpty(outputPath)) + if (installationRequest is null || + string.IsNullOrEmpty(installationRequest.ApkPath) || + string.IsNullOrEmpty(installationRequest.ZipPath)) { ShowUsage(); return; } - var adbPath = ExtractAdbFiles(); + var adbService = new AdbService(); - var server = new AdbServer(); - var result = server.StartServer(adbPath, restartServerIfNewer: false); - Console.WriteLine($"ADB сервер запущен: {result}"); + var apkInfo = ApkScrapper.GetApkInfo(installationRequest.ApkPath); + var zipName = Path.GetFileName(installationRequest.ZipPath); + var outputPath = + @$"/storage/emulated/0/Android/data/{apkInfo.PackageName}/files/{zipName}"; - adbClient = new AdbClient(); + // Подписка на события прогресса + adbService.ProgressChanged += OnProgressChanged; + adbService.StatusChanged += OnStatusChanged; - var devices = adbClient.GetDevices(); + // Получение информации об устройстве + var deviceInfo = adbService.GetDeviceInfo(); + Console.WriteLine($"Найдено устройство: {deviceInfo.SerialNumber}"); + Console.WriteLine($"Состояние: {deviceInfo.State}"); + Console.WriteLine($"Модель: {deviceInfo.Model} - {deviceInfo.Name}"); + Console.WriteLine(); - if (!devices.Any()) - { - Console.WriteLine("Устройства не найдены. Подключите Android-устройство и включите отладку по USB."); - return; - } + // Создание объекта для отслеживания прогресса + var progress = new Progress(OnProgressReport); - deviceData = devices.FirstOrDefault(); - Console.WriteLine($"Найдено устройство: {deviceData.Serial}"); - Console.WriteLine($"Состояние: {deviceData.State}"); - Console.WriteLine($"Имя устройства: {deviceData.Name} - {deviceData.Model}"); + // Установка APK + await adbService.InstallApkAsync(installationRequest.ApkPath, progress); + Console.WriteLine(); - InstallApk(apkPath); - CopyFileToDevice(zipPath, outputPath); + // Копирование файла + await adbService.CopyFileAsync(installationRequest.ZipPath, outputPath, progress); + Console.WriteLine(); + + Console.WriteLine("Операция завершена успешно!"); } catch (Exception ex) { @@ -59,6 +56,29 @@ class Program } } + private static void OnProgressChanged(object? sender, MetaforceInstaller.Core.Models.ProgressInfo e) + { + DrawProgressBar(e.PercentageComplete, e.BytesTransferred, e.TotalBytes); + } + + private static void OnStatusChanged(object? sender, string e) + { + Console.WriteLine(e); + } + + private static void OnProgressReport(MetaforceInstaller.Core.Models.ProgressInfo progressInfo) + { + if (progressInfo.TotalBytes > 0) + { + DrawProgressBar(progressInfo.PercentageComplete, progressInfo.BytesTransferred, progressInfo.TotalBytes); + } + else + { + // Для случаев без информации о байтах (например, установка APK) + DrawProgressBar(progressInfo.PercentageComplete, 0, 100); + } + } + static void ShowUsage() { Console.WriteLine("Использование:"); @@ -78,168 +98,36 @@ class Program Console.WriteLine(" MetaforceInstaller.exe -a app.apk -c data.zip -o /sdcard/data.zip"); } - - private static (string? apkPath, string? zipPath, string? outputPath) ParseArguments(string[] args) + private static void DrawProgressBar(int progress, long receivedBytes, long totalBytes) { - string apkPath = null; - string zipPath = null; - string outputPath = null; + Console.SetCursorPosition(0, Console.CursorTop); - for (int i = 0; i < args.Length; i++) + var barLength = 40; + var filledLength = (int)(barLength * progress / 100.0); + + var bar = "[" + new string('█', filledLength) + new string('░', barLength - filledLength) + "]"; + + string bytesText = ""; + if (totalBytes > 0) { - switch (args[i].ToLower()) - { - case "--apk": - case "-a": - if (i + 1 < args.Length) - { - apkPath = args[i + 1]; - i++; - } - - break; - - case "--content": - case "-c": - if (i + 1 < args.Length) - { - zipPath = args[i + 1]; - i++; - } - - break; - - case "--output": - case "-o": - if (i + 1 < args.Length) - { - outputPath = args[i + 1]; - i++; - } - - break; - - case "--help": - case "-h": - ShowUsage(); - Environment.Exit(0); - break; - } + bytesText = $" {FormatBytes(receivedBytes)} / {FormatBytes(totalBytes)}"; } - return (apkPath, zipPath, outputPath); + Console.Write($"\r{bar} {progress}%{bytesText}"); } - private static void ExtractResource(string resourceName, string outputPath) + private static string FormatBytes(long bytes) { - using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); - using var fileStream = File.Create(outputPath); - stream.CopyTo(fileStream); - } + string[] suffixes = ["B", "KB", "MB", "GB", "TB"]; + var counter = 0; + double number = bytes; - private static string ExtractAdbFiles() - { - var tempDir = Path.Combine(Path.GetTempPath(), "MetaforceInstaller", "adb"); - Directory.CreateDirectory(tempDir); - - var adbPath = Path.Combine(tempDir, "adb.exe"); - - if (!File.Exists(adbPath)) + while (Math.Round(number / 1024) >= 1) { - ExtractResource("MetaforceInstaller.Cli.adb.adb.exe", adbPath); - ExtractResource("MetaforceInstaller.Cli.adb.AdbWinApi.dll", Path.Combine(tempDir, "AdbWinApi.dll")); - ExtractResource("MetaforceInstaller.Cli.adb.AdbWinUsbApi.dll", Path.Combine(tempDir, "AdbWinUsbApi.dll")); + number /= 1024; + counter++; } - return adbPath; + return $"{number:N1} {suffixes[counter]}"; } - - private static void InstallApk(string apkPath) - { - try - { - if (!File.Exists(apkPath)) - { - Console.WriteLine($"APK файл не найден: {apkPath}"); - return; - } - - Console.WriteLine($"Установка APK: {apkPath}"); - - var packageManager = new PackageManager(adbClient, deviceData); - packageManager.InstallPackage(apkPath, new Action(o => { })); - - Console.WriteLine("APK успешно установлен!"); - } - catch (Exception ex) - { - Console.WriteLine($"Ошибка установки APK: {ex.Message}"); - } - } - -private static void CopyFileToDevice(string localPath, string remotePath) -{ - try - { - if (!File.Exists(localPath)) - { - Console.WriteLine($"Локальный файл не найден: {localPath}"); - return; - } - - Console.WriteLine($"Копирование файла {localPath} в {remotePath}"); - - var lastProgress = -1; - - using var fileStream = File.OpenRead(localPath); - var syncService = new SyncService(adbClient, deviceData); - - syncService.Push(fileStream, remotePath, UnixFileStatus.DefaultFileMode, DateTime.Now, - new Action(progress => - { - var currentProgress = progress.ProgressPercentage; - - if (currentProgress != lastProgress) - { - lastProgress = (int)currentProgress; - DrawProgressBar(lastProgress, progress.ReceivedBytesSize, progress.TotalBytesToReceive); - } - })); - - Console.WriteLine(); - Console.WriteLine("Файл успешно скопирован!"); - } - catch (Exception ex) - { - Console.WriteLine($"Ошибка копирования файла: {ex.Message}"); - } -} - -private static void DrawProgressBar(int progress, long receivedBytes, long totalBytes) -{ - Console.SetCursorPosition(0, Console.CursorTop); - - var barLength = 40; - var filledLength = (int)(barLength * progress / 100.0); - - var bar = "[" + new string('█', filledLength) + new string('░', barLength - filledLength) + "]"; - var bytesText = $" {FormatBytes(receivedBytes)} / {FormatBytes(totalBytes)}"; - - Console.Write($"\r{bar} {progress}%{bytesText}"); -} - -private static string FormatBytes(long bytes) -{ - string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; - var counter = 0; - double number = bytes; - - while (Math.Round(number / 1024) >= 1) - { - number /= 1024; - counter++; - } - - return $"{number:N1} {suffixes[counter]}"; -} } \ No newline at end of file diff --git a/MetaforceInstaller.Cli/Utils/ArgumentParser.cs b/MetaforceInstaller.Cli/Utils/ArgumentParser.cs new file mode 100644 index 0000000..ffdb561 --- /dev/null +++ b/MetaforceInstaller.Cli/Utils/ArgumentParser.cs @@ -0,0 +1,53 @@ +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.Cli.Utils; + +public static class ArgumentParser +{ + public static InstallationRequest? ParseArguments(string[] args) + { + var result = new InstallationRequest(); + + for (var i = 0; i < args.Length; i++) + { + switch (args[i].ToLower()) + { + case "--apk": + case "-a": + if (i + 1 < args.Length) + { + result.ApkPath = args[i + 1]; + i++; + } + + break; + + case "--content": + case "-c": + if (i + 1 < args.Length) + { + result.ZipPath = args[i + 1]; + i++; + } + + break; + + case "--output": + case "-o": + if (i + 1 < args.Length) + { + result.OutputPath = args[i + 1]; + i++; + } + + break; + + case "--help": + case "-h": + return null; + } + } + + return result; + } +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Intefaces/IAdbService.cs b/MetaforceInstaller.Core/Intefaces/IAdbService.cs new file mode 100644 index 0000000..444c237 --- /dev/null +++ b/MetaforceInstaller.Core/Intefaces/IAdbService.cs @@ -0,0 +1,18 @@ +using MetaforceInstaller.Core.Models; +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.Core.Intefaces; + +public interface IAdbService +{ + event EventHandler? ProgressChanged; + event EventHandler? StatusChanged; + + Task InstallApkAsync(string apkPath, IProgress? progress = null, CancellationToken cancellationToken = default); + Task CopyFileAsync(string localPath, string remotePath, IProgress? progress = null, CancellationToken cancellationToken = default); + DeviceInfo GetDeviceInfo(); + + // Синхронные версии для обратной совместимости + void InstallApk(string apkPath); + void CopyFile(string localPath, string remotePath); +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj b/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj new file mode 100644 index 0000000..0a85bfc --- /dev/null +++ b/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + diff --git a/MetaforceInstaller.Core/Models/ApkInfo.cs b/MetaforceInstaller.Core/Models/ApkInfo.cs new file mode 100644 index 0000000..9f042bf --- /dev/null +++ b/MetaforceInstaller.Core/Models/ApkInfo.cs @@ -0,0 +1,3 @@ +namespace MetaforceInstaller.Core.Models; + +public record ApkInfo(string PackageName, string VersionName, string VersionCode); \ No newline at end of file diff --git a/MetaforceInstaller.Core/Models/DeviceInfo.cs b/MetaforceInstaller.Core/Models/DeviceInfo.cs new file mode 100644 index 0000000..802cd3c --- /dev/null +++ b/MetaforceInstaller.Core/Models/DeviceInfo.cs @@ -0,0 +1,8 @@ +namespace MetaforceInstaller.Core.Models; + +public record DeviceInfo( + string SerialNumber, + string State, + string Model, + string Name +); \ No newline at end of file diff --git a/MetaforceInstaller.Core/Models/InstallationRequest.cs b/MetaforceInstaller.Core/Models/InstallationRequest.cs new file mode 100644 index 0000000..c64b271 --- /dev/null +++ b/MetaforceInstaller.Core/Models/InstallationRequest.cs @@ -0,0 +1,8 @@ +namespace MetaforceInstaller.Core.Models; + +public class InstallationRequest +{ + public string ApkPath { get; set; } + public string ZipPath { get; set; } + public string OutputPath { get; set; } +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Models/ProgressInfo.cs b/MetaforceInstaller.Core/Models/ProgressInfo.cs new file mode 100644 index 0000000..cc09a74 --- /dev/null +++ b/MetaforceInstaller.Core/Models/ProgressInfo.cs @@ -0,0 +1,19 @@ +namespace MetaforceInstaller.Core.Models; + +public class ProgressInfo +{ + public int PercentageComplete { get; set; } + public long BytesTransferred { get; set; } + public long TotalBytes { get; set; } + public string? Message { get; set; } + public string? CurrentFile { get; set; } + public ProgressType Type { get; set; } +} + +public enum ProgressType +{ + Installation, + FileCopy, + Extraction, + General +} diff --git a/MetaforceInstaller.Core/Services/AdbService.cs b/MetaforceInstaller.Core/Services/AdbService.cs new file mode 100644 index 0000000..e0e4920 --- /dev/null +++ b/MetaforceInstaller.Core/Services/AdbService.cs @@ -0,0 +1,194 @@ +using System.Reflection; +using AdvancedSharpAdbClient; +using AdvancedSharpAdbClient.DeviceCommands; +using AdvancedSharpAdbClient.Models; +using MetaforceInstaller.Core.Intefaces; +using MetaforceInstaller.Core.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace MetaforceInstaller.Core.Services; + +public class AdbService : IAdbService +{ + private readonly ILogger _logger; + private readonly AdbClient _adbClient; + private DeviceData _deviceData; + + public event EventHandler? ProgressChanged; + public event EventHandler? StatusChanged; + + public AdbService(ILogger? logger = null) + { + _logger = logger ?? new NullLogger(); + var adbPath = GetAdbPath(); + var server = new AdbServer(); + var serverStatus = server.StartServer(adbPath, restartServerIfNewer: false); + _adbClient = new AdbClient(); + RefreshDeviceData(); + } + + public void RefreshDeviceData() + { + var devices = _adbClient.GetDevices(); + _deviceData = devices.FirstOrDefault(); + } + + private void ExtractResource(string resourceName, string outputPath) + { + _logger.LogInformation($"Extracting resource: {resourceName} to {outputPath}"); + using var stream = Assembly.GetAssembly(typeof(AdbService)).GetManifestResourceStream(resourceName); + using var fileStream = File.Create(outputPath); + stream.CopyTo(fileStream); + _logger.LogInformation($"Resource extracted: {resourceName} to {outputPath}"); + } + + private string GetAdbPath() + { + var tempDir = Path.Combine(Path.GetTempPath(), "MetaforceInstaller", "adb"); + Directory.CreateDirectory(tempDir); + + var adbPath = Path.Combine(tempDir, "adb.exe"); + + if (File.Exists(adbPath)) return adbPath; + ExtractResource("MetaforceInstaller.Core.adb.adb.exe", adbPath); + ExtractResource("MetaforceInstaller.Core.adb.AdbWinApi.dll", Path.Combine(tempDir, "AdbWinApi.dll")); + ExtractResource("MetaforceInstaller.Core.adb.AdbWinUsbApi.dll", Path.Combine(tempDir, "AdbWinUsbApi.dll")); + + return adbPath; + } + + private void OnProgressChanged(ProgressInfo progressInfo) + { + ProgressChanged?.Invoke(this, progressInfo); + } + + private void OnStatusChanged(string status) + { + StatusChanged?.Invoke(this, status); + } + + public void InstallApk(string apkPath) + { + InstallApkAsync(apkPath).Wait(); + } + + public async Task InstallApkAsync(string apkPath, IProgress? progress = null, + CancellationToken cancellationToken = default) + { + try + { + if (!File.Exists(apkPath)) + { + _logger.LogCritical("Error: Could not find APK file."); + return; + } + + OnStatusChanged("Начинаем установку APK..."); + _logger.LogInformation($"Installing APK: {apkPath}"); + + progress?.Report(new ProgressInfo + { + PercentageComplete = 0, + Message = "Подготовка к установке APK...", + Type = ProgressType.Installation, + CurrentFile = Path.GetFileName(apkPath) + }); + + var packageManager = new PackageManager(_adbClient, _deviceData); + + await Task.Run(() => + { + packageManager.InstallPackage(apkPath, installProgress => + { + var progressInfo = new ProgressInfo + { + PercentageComplete = (int)installProgress.UploadProgress, + Message = $"Установка APK: {installProgress.UploadProgress:F1}%", + Type = ProgressType.Installation, + CurrentFile = Path.GetFileName(apkPath) + }; + + progress?.Report(progressInfo); + OnProgressChanged(progressInfo); + }); + }, cancellationToken); + + OnStatusChanged("APK успешно установлен!"); + _logger.LogInformation("APK successfully installed!"); + } + catch (Exception ex) + { + _logger.LogCritical($"Error: {ex.Message}"); + throw; + } + } + + public void CopyFile(string localPath, string remotePath) + { + CopyFileAsync(localPath, remotePath).Wait(); + } + + public async Task CopyFileAsync(string localPath, string remotePath, IProgress? progress = null, + CancellationToken cancellationToken = default) + { + try + { + if (!File.Exists(localPath)) + { + _logger.LogCritical($"Error: Could not find file: {localPath}"); + return; + } + + OnStatusChanged("Начинаем копирование файла..."); + _logger.LogInformation($"Copying file: {localPath} to {remotePath}"); + + var fileInfo = new FileInfo(localPath); + + progress?.Report(new ProgressInfo + { + PercentageComplete = 0, + Message = "Подготовка к копированию файла...", + Type = ProgressType.FileCopy, + CurrentFile = Path.GetFileName(localPath), + TotalBytes = fileInfo.Length + }); + + await Task.Run(() => + { + using var fileStream = File.OpenRead(localPath); + var syncService = new SyncService(_adbClient, _deviceData); + + syncService.Push(fileStream, remotePath, UnixFileStatus.DefaultFileMode, DateTime.Now, + copyProgress => + { + var progressInfo = new ProgressInfo + { + PercentageComplete = (int)copyProgress.ProgressPercentage, + BytesTransferred = copyProgress.ReceivedBytesSize, + TotalBytes = copyProgress.TotalBytesToReceive, + Message = $"Копирование: {copyProgress.ProgressPercentage:F1}%", + Type = ProgressType.FileCopy, + CurrentFile = Path.GetFileName(localPath) + }; + + progress?.Report(progressInfo); + OnProgressChanged(progressInfo); + }); + }, cancellationToken); + + OnStatusChanged("Файл успешно скопирован!"); + _logger.LogInformation("File successfully copied!"); + } + catch (Exception ex) + { + _logger.LogCritical($"Error: {ex.Message}"); + throw; + } + } + + public DeviceInfo GetDeviceInfo() + { + return new DeviceInfo(_deviceData.Serial, _deviceData.State.ToString(), _deviceData.Model, _deviceData.Name); + } +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Services/ApkScrapper.cs b/MetaforceInstaller.Core/Services/ApkScrapper.cs new file mode 100644 index 0000000..c06c543 --- /dev/null +++ b/MetaforceInstaller.Core/Services/ApkScrapper.cs @@ -0,0 +1,19 @@ +using AlphaOmega.Debug; +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.Core.Services; + +public static class ApkScrapper +{ + public static ApkInfo GetApkInfo(string apkPath) + { + using var apk = new ApkFile(apkPath); + if (apk is { IsValid: true, AndroidManifest: not null }) + { + return new ApkInfo(apk.AndroidManifest.Package, apk.AndroidManifest.VersionName, + apk.AndroidManifest.VersionCode); + } + + throw new Exception("Invalid APK file"); + } +} \ No newline at end of file diff --git a/MetaforceInstaller.Cli/adb/AdbWinApi.dll b/MetaforceInstaller.Core/adb/AdbWinApi.dll similarity index 100% rename from MetaforceInstaller.Cli/adb/AdbWinApi.dll rename to MetaforceInstaller.Core/adb/AdbWinApi.dll diff --git a/MetaforceInstaller.Cli/adb/AdbWinUsbApi.dll b/MetaforceInstaller.Core/adb/AdbWinUsbApi.dll similarity index 100% rename from MetaforceInstaller.Cli/adb/AdbWinUsbApi.dll rename to MetaforceInstaller.Core/adb/AdbWinUsbApi.dll diff --git a/MetaforceInstaller.Cli/adb/adb.exe b/MetaforceInstaller.Core/adb/adb.exe similarity index 100% rename from MetaforceInstaller.Cli/adb/adb.exe rename to MetaforceInstaller.Core/adb/adb.exe diff --git a/MetaforceInstaller.UI/App.axaml b/MetaforceInstaller.UI/App.axaml new file mode 100644 index 0000000..d947dd9 --- /dev/null +++ b/MetaforceInstaller.UI/App.axaml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/MetaforceInstaller.UI/App.axaml.cs b/MetaforceInstaller.UI/App.axaml.cs new file mode 100644 index 0000000..16212ac --- /dev/null +++ b/MetaforceInstaller.UI/App.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace MetaforceInstaller.UI; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/MetaforceInstaller.UI/MainWindow.axaml b/MetaforceInstaller.UI/MainWindow.axaml new file mode 100644 index 0000000..c1e1044 --- /dev/null +++ b/MetaforceInstaller.UI/MainWindow.axaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + +