diff --git a/UI/AndroidUpdate/AndroidUpdate.slnx b/UI/AndroidUpdate/AndroidUpdate.slnx new file mode 100644 index 0000000..0c4c7b4 --- /dev/null +++ b/UI/AndroidUpdate/AndroidUpdate.slnx @@ -0,0 +1,3 @@ + + + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/AndroidApp.cs b/UI/AndroidUpdate/src/AndroidUpdate.Android/AndroidApp.cs new file mode 100644 index 0000000..169a1dd --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/AndroidApp.cs @@ -0,0 +1,33 @@ +using Android.App; +using Android.Runtime; +using AndroidUpdate.ViewModels; +using Avalonia; +using Avalonia.Android; + +namespace AndroidUpdate.Android; + +[global::Android.App.Application] +public class AndroidApp : AvaloniaAndroidApplication +{ + protected AndroidApp(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + // Read real app version from package manager as early as possible + try + { + var pkgInfo = PackageManager?.GetPackageInfo(PackageName!, 0); + if (pkgInfo?.VersionName != null) + App.DeviceVersion = pkgInfo.VersionName; + } + catch { /* fallback to default "1.0.0.0" */ } + } + + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + // Register the Android-specific handler factory before Avalonia starts + App.HandlerFactory = (packageInfo, currentVersion) => + new Services.AndroidUpdateHandler(packageInfo, currentVersion); + + return base.CustomizeAppBuilder(builder) + .WithInterFont(); + } +} diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/AndroidUpdate.Android.csproj b/UI/AndroidUpdate/src/AndroidUpdate.Android/AndroidUpdate.Android.csproj new file mode 100644 index 0000000..6954e51 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/AndroidUpdate.Android.csproj @@ -0,0 +1,42 @@ + + + Exe + net10.0-android + 23 + enable + enable + latest + com.generalupdate.androidupdate + 7 + 1.0.0.0 + AndroidUpdate + apk + false + true + + + + + + + + + None + All + + + + + Resources\drawable\Icon.png + + + + + + + + + libs\GeneralUpdate.Avalonia.Android.dll + + + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/App.axaml b/UI/AndroidUpdate/src/AndroidUpdate.Android/App.axaml new file mode 100644 index 0000000..ecdf2ed --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/App.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/App.axaml.cs b/UI/AndroidUpdate/src/AndroidUpdate.Android/App.axaml.cs new file mode 100644 index 0000000..072edfb --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/App.axaml.cs @@ -0,0 +1,59 @@ +using System; +using System.Net.Http; +using AndroidUpdate.ViewModels; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace AndroidUpdate; + +public partial class App : Avalonia.Application +{ + /// + /// Static factory for creating platform-specific update handlers. + /// Set by the Android project (or other platform projects) during startup. + /// + public static Func? HandlerFactory { get; set; } + + /// + /// The device's currently installed app version. + /// Set by the platform project on startup; falls back to "1.0.0.0". + /// + public static string DeviceVersion { get; set; } = "1.0.0.0"; + + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(30); + + MainViewViewModel CreateViewModel() => + new(httpClient, (pkg, ver) => + { + if (HandlerFactory == null) + throw new InvalidOperationException( + "HandlerFactory not set. Ensure the platform project initializes it."); + return HandlerFactory(pkg, ver); + }); + + if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactory) + { + // Android: use MainViewFactory for Activity-based lifetime + singleViewFactory.MainViewFactory = () => + new Views.MainView { DataContext = CreateViewModel() }; + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + singleViewPlatform.MainView = new Views.MainView + { + DataContext = CreateViewModel() + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Icon.png b/UI/AndroidUpdate/src/AndroidUpdate.Android/Icon.png new file mode 100644 index 0000000..3c39845 Binary files /dev/null and b/UI/AndroidUpdate/src/AndroidUpdate.Android/Icon.png differ diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/MainActivity.cs b/UI/AndroidUpdate/src/AndroidUpdate.Android/MainActivity.cs new file mode 100644 index 0000000..7875452 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/MainActivity.cs @@ -0,0 +1,15 @@ +using Android.App; +using Android.Content.PM; +using Avalonia; +using Avalonia.Android; + +namespace AndroidUpdate.Android; + +[Activity( + Label = "AndroidUpdate", + Theme = "@style/MyTheme.NoActionBar", + MainLauncher = true, + ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] +public class MainActivity : AvaloniaMainActivity +{ +} diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Properties/AndroidManifest.xml b/UI/AndroidUpdate/src/AndroidUpdate.Android/Properties/AndroidManifest.xml new file mode 100644 index 0000000..27ec047 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Properties/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable-night-v31/avalonia_anim.xml b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable-night-v31/avalonia_anim.xml new file mode 100644 index 0000000..dde4b5a --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable-night-v31/avalonia_anim.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable-v31/avalonia_anim.xml b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable-v31/avalonia_anim.xml new file mode 100644 index 0000000..94f27d9 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable-v31/avalonia_anim.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable/splash_screen.xml b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable/splash_screen.xml new file mode 100644 index 0000000..2e920b4 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable/splash_screen.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values-night/colors.xml b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values-night/colors.xml new file mode 100644 index 0000000..3d47b6f --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #212121 + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values-v31/styles.xml b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values-v31/styles.xml new file mode 100644 index 0000000..d5ecec4 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values/colors.xml b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values/colors.xml new file mode 100644 index 0000000..59279d5 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values/styles.xml b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values/styles.xml new file mode 100644 index 0000000..6e534de --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values/styles.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/xml/file_paths.xml b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/xml/file_paths.xml new file mode 100644 index 0000000..767c20f --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/xml/network_security_config.xml b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/xml/network_security_config.xml new file mode 100644 index 0000000..c7f8784 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/xml/network_security_config.xml @@ -0,0 +1,7 @@ + + + + 192.168.50.204 + localhost + + diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Services/AndroidUpdateHandler.cs b/UI/AndroidUpdate/src/AndroidUpdate.Android/Services/AndroidUpdateHandler.cs new file mode 100644 index 0000000..8136482 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Services/AndroidUpdateHandler.cs @@ -0,0 +1,118 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Android.Content; +using Android.OS; +using AndroidUpdate.ViewModels; +using GeneralUpdate.Avalonia.Android; +using GeneralUpdate.Avalonia.Android.Abstractions; +using GeneralUpdate.Avalonia.Android.Models; + +namespace AndroidUpdate.Android.Services; + +public sealed class AndroidUpdateHandler : IAndroidUpdateHandler +{ + private readonly IAndroidBootstrap _bootstrap; + private readonly UpdatePackageDto _packageInfo; + private bool _disposed; + + public event EventHandler? ProgressChanged; + public event EventHandler? StatusChanged; + + public AndroidUpdateHandler(UpdatePackageDto packageInfo, string currentVersion) + { + _packageInfo = packageInfo ?? throw new ArgumentNullException(nameof(packageInfo)); + _ = currentVersion ?? throw new ArgumentNullException(nameof(currentVersion)); + + var options = new AndroidUpdateOptions + { + FileProviderAuthority = "com.generalupdate.androidupdate.fileprovider" + }; + + _bootstrap = GeneralUpdateBootstrap.CreateDefault(options); + WireEvents(); + } + + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + try + { + var updatePackage = new UpdatePackageInfo + { + Version = _packageInfo.Version, + DownloadUrl = _packageInfo.DownloadUrl, + Sha256 = _packageInfo.Sha256, + FileSize = _packageInfo.FileSize, + FileName = $"app_update_{_packageInfo.Version}.apk", + IsForced = _packageInfo.IsForced + }; + + // Step 1: Download and verify + StatusChanged?.Invoke(this, "Downloading package..."); + var downloadResult = await _bootstrap.DownloadAndVerifyAsync(updatePackage, cancellationToken); + if (!downloadResult.Success) + { + StatusChanged?.Invoke(this, $"Download failed: {downloadResult.Message} (reason: {downloadResult.FailureReason})"); + return false; + } + + StatusChanged?.Invoke(this, "Download OK, checking install permission..."); + + // Step 2: Check "Install unknown apps" permission (Android 8+) + if (Build.VERSION.SdkInt >= BuildVersionCodes.O) + { + var ctx = global::Android.App.Application.Context; + var pm = ctx.PackageManager; + if (pm != null && !pm.CanRequestPackageInstalls()) + { + StatusChanged?.Invoke(this, "Tap Download again after allowing install permission."); + var intent = new Intent(global::Android.Provider.Settings.ActionManageUnknownAppSources) + .SetData(global::Android.Net.Uri.Parse("package:" + ctx.PackageName)); + intent.AddFlags(ActivityFlags.NewTask); + ctx.StartActivity(intent); + return false; + } + } + + // Step 3: Launch Android installer + StatusChanged?.Invoke(this, "Installing..."); + var installResult = await _bootstrap.LaunchInstallerAsync(updatePackage, downloadResult.FilePath!, cancellationToken); + if (!installResult.Success) + { + StatusChanged?.Invoke(this, $"Install failed: {installResult.Message} (reason: {installResult.FailureReason})"); + return false; + } + return true; + } + catch (System.OperationCanceledException) + { + StatusChanged?.Invoke(this, "Cancelled."); + return false; + } + catch (Exception ex) + { + StatusChanged?.Invoke(this, $"Error: {ex.Message}"); + return false; + } + } + + private void WireEvents() + { + _bootstrap.AddListenerDownloadProgressChanged += (_, args) => + { + ProgressChanged?.Invoke(this, args.ProgressPercentage); + }; + + _bootstrap.AddListenerUpdateFailed += (_, args) => + { + StatusChanged?.Invoke(this, $"Failed: {args.Result.Message}"); + }; + } + + public void Dispose() + { + if (_disposed) return; + _bootstrap.Dispose(); + _disposed = true; + } +} diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewLocator.cs b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewLocator.cs new file mode 100644 index 0000000..5e5a2e8 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewLocator.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using AndroidUpdate.ViewModels; + +namespace AndroidUpdate; + +[RequiresUnreferencedCode( + "Default implementation of ViewLocator involves reflection which may be trimmed away.", + Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")] +public class ViewLocator : IDataTemplate +{ + public Control? Build(object? param) + { + if (param is null) + return null; + + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/IAndroidUpdateHandler.cs b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/IAndroidUpdateHandler.cs new file mode 100644 index 0000000..6225e8f --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/IAndroidUpdateHandler.cs @@ -0,0 +1,13 @@ +namespace AndroidUpdate.ViewModels; + +/// +/// Abstraction for the platform-specific update download/install handler. +/// The Android project provides the implementation using GeneralUpdate.Avalonia.Android. +/// +public interface IAndroidUpdateHandler : IDisposable +{ + event EventHandler? ProgressChanged; + event EventHandler? StatusChanged; + + Task ExecuteAsync(CancellationToken cancellationToken = default); +} diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/MainViewViewModel.cs b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/MainViewViewModel.cs new file mode 100644 index 0000000..03ff3ba --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/MainViewViewModel.cs @@ -0,0 +1,273 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace AndroidUpdate.ViewModels; + +public partial class MainViewViewModel : ViewModelBase +{ + private readonly HttpClient _httpClient; + private readonly Func _handlerFactory; + + private IAndroidUpdateHandler? _currentHandler; + private CancellationTokenSource? _cts; + + // ── Bindable properties ──────────────────────────────────────── + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(CheckUpdateCommand))] + private string _currentVersion = AndroidUpdate.App.DeviceVersion; + + [ObservableProperty] + private string _statusText = "Ready"; + + [ObservableProperty] + private double _downloadProgress; + + [ObservableProperty] + private string _progressText = ""; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(CheckUpdateCommand))] + private bool _isChecking; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(CheckUpdateCommand))] + [NotifyCanExecuteChangedFor(nameof(DownloadUpdateCommand))] + private bool _isDownloading; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(DownloadUpdateCommand))] + private bool _hasUpdate; + + [ObservableProperty] + private bool _updateChecked; + + [ObservableProperty] + private bool _showNoUpdateMessage; + + [ObservableProperty] + private string _newVersion = ""; + + [ObservableProperty] + private string _updateDescription = ""; + + // ── Dialog ───────────────────────────────────────────────────── + + [ObservableProperty] + private bool _isDialogVisible; + + [ObservableProperty] + private string _dialogTitle = ""; + + [ObservableProperty] + private string _dialogMessage = ""; + + private TaskCompletionSource? _dialogTcs; + + private UpdatePackageDto? _pendingUpdate; + + public MainViewViewModel(HttpClient httpClient, Func handlerFactory) + { + _httpClient = httpClient; + _handlerFactory = handlerFactory; + } + + /// + /// Configurable server URL for the sample. Defaults to localhost:5000; + /// override via environment variable ANDROID_UPDATE_SERVER_URL. + /// + private static string ServerBaseUrl => + Environment.GetEnvironmentVariable("ANDROID_UPDATE_SERVER_URL") + ?? "http://localhost:5000"; + + // ── Dialog helpers ───────────────────────────────────────────── + + [RelayCommand] + private void DialogConfirm() + { + IsDialogVisible = false; + _dialogTcs?.TrySetResult(true); + } + + [RelayCommand] + private void DialogCancel() + { + IsDialogVisible = false; + _dialogTcs?.TrySetResult(false); + } + + private Task ShowConfirmAsync(string title, string message) + { + DialogTitle = title; + DialogMessage = message; + IsDialogVisible = true; + _dialogTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + return _dialogTcs.Task; + } + + // ── Commands ─────────────────────────────────────────────────── + + private bool CanCheck => !IsChecking && !IsDownloading; + + [RelayCommand(CanExecute = nameof(CanCheck))] + private async Task CheckUpdateAsync() + { + var confirmed = await ShowConfirmAsync( + "GeneralUpdate.Avalonia", + "This app uses GeneralUpdate.Avalonia to check for Android APK updates.\n\n" + + "The library provides version comparison, resumable download with SHA256 verification, and automated APK installation.\n\n" + + "Do you want to check for updates now?"); + + if (!confirmed) return; + + IsChecking = true; + HasUpdate = false; + UpdateChecked = false; + ShowNoUpdateMessage = false; + StatusText = "Checking for updates..."; + _cts = new CancellationTokenSource(); + + try + { + var result = await CheckServerAsync(_cts.Token); + + if (result == null) + { + StatusText = "No update available."; + UpdateChecked = true; + ShowNoUpdateMessage = true; + return; + } + + NewVersion = result.Version; + UpdateDescription = result.Description ?? $"Version {result.Version} is available."; + _pendingUpdate = result; + HasUpdate = true; + UpdateChecked = true; + StatusText = $"Update v{result.Version} available!"; + } + catch (OperationCanceledException) + { + StatusText = "Cancelled."; + } + catch (Exception ex) + { + StatusText = $"Error: {ex.Message}"; + } + finally + { + IsChecking = false; + _cts?.Dispose(); + _cts = null; + } + } + + private bool CanDownload => !IsDownloading && HasUpdate; + + [RelayCommand(CanExecute = nameof(CanDownload))] + private async Task DownloadUpdateAsync() + { + if (_pendingUpdate == null) return; + + var confirmed = await ShowConfirmAsync( + "GeneralUpdate.Avalonia", + $"GeneralUpdate.Avalonia will now download version {_pendingUpdate.Version} APK,\n" + + $"verify its SHA256 checksum, and launch the Android package installer.\n\n" + + $"Target version: {_pendingUpdate.Version}\n" + + $"Package size: {_pendingUpdate.FileSize / 1024 / 1024:F1} MB\n\n" + + "Do you want to proceed with the update?"); + + if (!confirmed) return; + + IsDownloading = true; + HasUpdate = false; + StatusText = "Downloading..."; + DownloadProgress = 0; + ProgressText = "0%"; + _cts = new CancellationTokenSource(); + + try + { + _currentHandler = _handlerFactory(_pendingUpdate, CurrentVersion); + + _currentHandler.ProgressChanged += (_, p) => + { + DownloadProgress = p; + ProgressText = $"{p:F1}%"; + }; + + _currentHandler.StatusChanged += (_, s) => + { + StatusText = s; + }; + + var success = await _currentHandler.ExecuteAsync(_cts.Token); + + if (success) + { + CurrentVersion = _pendingUpdate.Version; + _pendingUpdate = null; // clear so retry won't use stale data + StatusText = $"Updated to v{CurrentVersion}!"; + DownloadProgress = 100; + ProgressText = "Done"; + } + } + catch (OperationCanceledException) + { + StatusText = "Cancelled."; + } + finally + { + IsDownloading = false; + _cts?.Dispose(); + _cts = null; + _currentHandler?.Dispose(); + _currentHandler = null; + + // Restore the Download button if an update is still available for retry + if (_pendingUpdate != null) + HasUpdate = true; + } + } + + // ── Server interaction ───────────────────────────────────────── + + private async Task CheckServerAsync(CancellationToken ct) + { + var request = new + { + Version = CurrentVersion, + AppType = 1, + Platform = 4, + ProductId = "2d974e2a-31e6-4887-9bb1-b4689e98c77a" + }; + + var response = await _httpClient.PostAsJsonAsync( + $"{ServerBaseUrl}/Upgrade/Verification", + request, ct); + + response.EnsureSuccessStatusCode(); + + using var doc = await JsonDocument.ParseAsync( + await response.Content.ReadAsStreamAsync(ct), cancellationToken: ct); + + var body = doc.RootElement.GetProperty("body"); + if (body.GetArrayLength() == 0) return null; + + var entry = body[0]; + return new UpdatePackageDto( + Version: entry.GetProperty("version").GetString()!, + DownloadUrl: entry.GetProperty("url").GetString()!, + Sha256: entry.GetProperty("hash").GetString()!, + FileSize: entry.TryGetProperty("size", out var s) ? s.GetInt64() : 0, + Description: entry.TryGetProperty("name", out var n) ? n.GetString() : null, + IsForced: entry.TryGetProperty("isForcibly", out var f) && f.GetBoolean() + ); + } +} diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/UpdatePackageDto.cs b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/UpdatePackageDto.cs new file mode 100644 index 0000000..8ba7af0 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/UpdatePackageDto.cs @@ -0,0 +1,12 @@ +namespace AndroidUpdate.ViewModels; + +/// +/// Describes an update package returned by the server's verification endpoint. +/// +public sealed record UpdatePackageDto( + string Version, + string DownloadUrl, + string Sha256, + long FileSize, + string? Description, + bool IsForced); diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/ViewModelBase.cs b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..b96636b --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace AndroidUpdate.ViewModels; + +public abstract class ViewModelBase : ObservableObject +{ +} diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/Views/MainView.axaml b/UI/AndroidUpdate/src/AndroidUpdate.Android/Views/MainView.axaml new file mode 100644 index 0000000..f201bc3 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Views/MainView.axaml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +