Skip to content

Commit 53efc31

Browse files
JusterZhuclaude
andauthored
feat: add MAUI Android update sample (#119)
* refactor: remove UpgradeMode from sample server DTOs and logic - Remove UpgradeMode from VerifyDTO and VerificationResultDTO - Remove UpgradeMode-based filtering and logging in Program.cs Co-Authored-By: Claude <noreply@anthropic.com> * feat: add Android APK update sample with GeneralUpdate.Avalonia - Add UI/AndroidUpdate/ sample project (net10.0-android) - Avalonia UI with MVVM pattern (CommunityToolkit.Mvvm) - Confirmation dialogs for check and download actions - Real-time progress bar with percentage display - App version read from PackageManager (not hardcoded) - Permission check for Android 8+ unknown app sources - Reference GeneralUpdate.Avalonia.Android via compiled DLL (libs/GeneralUpdate.Avalonia.Android.dll) - Modify Server/Program.cs to support .apk format downloads - Use Format field from versions.json for file extension - Search both .zip and .apk in hash lookup - Support non-.zip file extensions in hash computation - Add Android platform entry (Platform=4) to versions.json - Add BaseUrl and Urls config to Server appsettings.json Co-Authored-By: Claude <noreply@anthropic.com> * fix: address Copilot review comments - Rename PackageInfo to UpdatePackageDto to avoid collision with Android SDK type - Use TaskCreationOptions.RunContinuationsAsynchronously for dialog TCS - Restore HasUpdate on download failure so retry is possible - Reset _pendingUpdate only after successful update - Replace global cleartext flag with targeted network_security_config - Tighten FileProvider paths to dedicated update/ subfolder only - Change BaseUrl to localhost (LAN IP was environment-specific) - Make ServerBaseUrl configurable via ANDROID_UPDATE_SERVER_URL env var Co-Authored-By: Claude <noreply@anthropic.com> * feat: add MAUI Android update sample with GeneralUpdate.Maui.Android Adds a complete .NET MAUI Android sample demonstrating auto-update integration with GeneralUpdate.Maui.Android library. Includes: - MauiUpdate.Android client app with MVVM pattern (CommunityToolkit.Mvvm) - Update server (ASP.NET Minimal API) for version check and APK delivery - Full update flow: check → download → verify SHA256 → install - Platform config: AndroidManifest, FileProvider, network security - Real device version detection via PackageManager - Support for 'Install unknown apps' permission flow on Android 8+ The sample mirrors the pattern from UI/SemiUrsa (Avalonia) and UI/AndroidUpdate (Avalonia.Android) samples. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: JusterChu <juster.chu@foxmail.com> * chore: add MauiUpdate.slnx solution file Adds a .slnx solution file referencing the client and server projects for easier management in IDEs. Co-Authored-By: Claude <noreply@anthropic.com> * fix: address Copilot review feedback - Fix nuget.config: use relative path for local feed, keep nuget.org - Fix .csproj: use explicit MAUI version instead of unset - Fix network_security_config: remove hardcoded LAN IP, remove user CAs - Fix MainViewModel: use generic localhost default, use Format extension - Fix server endpoint: enable range processing, remove unused variable Co-Authored-By: Claude <noreply@anthropic.com> --------- Signed-off-by: JusterChu <juster.chu@foxmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent bcff8a3 commit 53efc31

22 files changed

Lines changed: 1018 additions & 0 deletions

UI/MauiUpdate/MauiUpdate.slnx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<Solution>
2+
<Project Path="src/MauiUpdate.Android/MauiUpdate.Android.csproj" />
3+
<Project Path="server/Server.csproj" />
4+
</Solution>

UI/MauiUpdate/nuget.config

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<configuration>
3+
<packageSources>
4+
<add key="local-packages" value="../local-packages" />
5+
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
6+
</packageSources>
7+
</configuration>

UI/MauiUpdate/server/Program.cs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
var builder = WebApplication.CreateBuilder(args);
5+
builder.WebHost.UseUrls("http://0.0.0.0:5000");
6+
7+
// Allow all origins for local testing
8+
builder.Services.AddCors(options =>
9+
{
10+
options.AddDefaultPolicy(policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
11+
});
12+
13+
var app = builder.Build();
14+
app.UseCors();
15+
16+
var packagesDir = Path.Combine(app.Environment.ContentRootPath, "packages");
17+
Directory.CreateDirectory(packagesDir);
18+
19+
// GET /packages/{filename} - Serve APK files with range support
20+
app.MapGet("/packages/{filename}", (string filename) =>
21+
{
22+
var sanitized = Path.GetFileName(filename);
23+
var filePath = Path.Combine(packagesDir, sanitized);
24+
if (!File.Exists(filePath))
25+
{
26+
return Results.NotFound(new { error = "Package not found.", filename = sanitized });
27+
}
28+
29+
return Results.File(
30+
filePath,
31+
contentType: "application/vnd.android.package-archive",
32+
fileDownloadName: sanitized,
33+
enableRangeProcessing: true);
34+
});
35+
36+
// POST /Upgrade/Verification - Version check API (matches GeneralUpdate server format)
37+
app.MapPost("/Upgrade/Verification", async (HttpContext context) =>
38+
{
39+
try
40+
{
41+
using var reader = new StreamReader(context.Request.Body);
42+
var body = await reader.ReadToEndAsync();
43+
44+
// Parse the request to extract current version
45+
using var doc = JsonDocument.Parse(body);
46+
var requestVersion = doc.RootElement.GetProperty("Version").GetString() ?? "0.0.0.0";
47+
48+
// Read versions.json
49+
var versionsPath = Path.Combine(packagesDir, "versions.json");
50+
if (!File.Exists(versionsPath))
51+
{
52+
return Results.Ok(new { code = 1, message = "No versions file.", body = new List<PackageEntry>() });
53+
}
54+
55+
var json = await File.ReadAllTextAsync(versionsPath);
56+
var packageOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
57+
var packages = JsonSerializer.Deserialize<List<PackageEntry>>(json, packageOptions);
58+
59+
if (packages is null || packages.Count == 0)
60+
{
61+
return Results.Ok(new { code = 1, message = "No packages configured.", body = new List<PackageEntry>() });
62+
}
63+
64+
// Find the latest version that is newer than the request
65+
var current = Version.TryParse(requestVersion, out var cv) ? cv : new Version(0, 0, 0, 0);
66+
var available = packages
67+
.Select(p => new { Package = p, ParsedVersion = Version.TryParse(p.Version, out var v) ? v : new Version(0, 0, 0, 0) })
68+
.Where(x => x.ParsedVersion > current)
69+
.OrderByDescending(x => x.ParsedVersion)
70+
.ToList();
71+
72+
if (available.Count == 0)
73+
{
74+
return Results.Ok(new { code = 1, message = "No updates available.", body = new List<PackageEntry>() });
75+
}
76+
77+
var latest = available[0].Package;
78+
return Results.Ok(new { code = 0, message = "Success", body = new[] { latest } });
79+
}
80+
catch (Exception ex)
81+
{
82+
return Results.Ok(new { code = -1, message = ex.Message, body = new List<PackageEntry>() });
83+
}
84+
});
85+
86+
// POST /Upgrade/Report - Report update result
87+
app.MapPost("/Upgrade/Report", async (HttpContext context) =>
88+
{
89+
using var reader = new StreamReader(context.Request.Body);
90+
var body = await reader.ReadToEndAsync();
91+
Console.WriteLine($"[Report] {body}");
92+
return Results.Ok(new { code = 0, message = "Report received." });
93+
});
94+
95+
// Health check
96+
app.MapGet("/", () => Results.Ok(new { status = "running", server = "MauiUpdate Server" }));
97+
98+
Console.WriteLine("MauiUpdate Server running on http://0.0.0.0:5000");
99+
app.Run();
100+
101+
/// <summary>
102+
/// Package entry matching the versions.json schema.
103+
/// JsonPropertyName attributes ensure correct serialization for the client.
104+
/// </summary>
105+
internal sealed record PackageEntry
106+
{
107+
[JsonPropertyName("PacketName")] public string PacketName { get; init; } = string.Empty;
108+
[JsonPropertyName("Hash")] public string Hash { get; init; } = string.Empty;
109+
[JsonPropertyName("Version")] public string Version { get; init; } = string.Empty;
110+
[JsonPropertyName("PubTime")] public string PubTime { get; init; } = string.Empty;
111+
[JsonPropertyName("AppType")] public int AppType { get; init; }
112+
[JsonPropertyName("Platform")] public int Platform { get; init; }
113+
[JsonPropertyName("ProductId")] public string ProductId { get; init; } = string.Empty;
114+
[JsonPropertyName("IsForcibly")] public bool IsForcibly { get; init; }
115+
[JsonPropertyName("Format")] public string Format { get; init; } = ".apk";
116+
[JsonPropertyName("Size")] public long Size { get; init; }
117+
}

UI/MauiUpdate/server/Server.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
<PropertyGroup>
3+
<TargetFramework>net10.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<RootNamespace>MauiUpdate.Server</RootNamespace>
7+
</PropertyGroup>
8+
</Project>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
x:Class="MauiUpdate.App">
5+
<Application.Resources>
6+
<ResourceDictionary>
7+
<ResourceDictionary.MergedDictionaries>
8+
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
9+
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
10+
</ResourceDictionary.MergedDictionaries>
11+
</ResourceDictionary>
12+
</Application.Resources>
13+
</Application>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using MauiUpdate.Views;
2+
3+
namespace MauiUpdate;
4+
5+
public partial class App : Application
6+
{
7+
public App(MainPage mainPage)
8+
{
9+
InitializeComponent();
10+
_mainPage = mainPage;
11+
}
12+
13+
private readonly MainPage _mainPage;
14+
15+
protected override Window CreateWindow(IActivationState? activationState)
16+
{
17+
return new Window(_mainPage)
18+
{
19+
Title = "MauiUpdate",
20+
};
21+
}
22+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using MauiUpdate.Services;
2+
using MauiUpdate.ViewModels;
3+
using MauiUpdate.Views;
4+
5+
namespace MauiUpdate;
6+
7+
public static class MauiProgram
8+
{
9+
public static MauiApp CreateMauiApp()
10+
{
11+
var builder = MauiApp.CreateBuilder();
12+
builder
13+
.UseMauiApp<App>()
14+
.ConfigureFonts(fonts => { });
15+
16+
// Register HttpClient
17+
builder.Services.AddSingleton<HttpClient>(_ =>
18+
{
19+
var client = new HttpClient();
20+
client.Timeout = TimeSpan.FromMinutes(10);
21+
return client;
22+
});
23+
24+
// Register services
25+
builder.Services.AddSingleton<MauiUpdateHandler>();
26+
27+
// Register ViewModels
28+
builder.Services.AddSingleton<MainViewModel>();
29+
30+
// Register Pages
31+
builder.Services.AddSingleton<MainPage>();
32+
33+
return builder.Build();
34+
}
35+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0-android</TargetFramework>
6+
<SupportedOSPlatformVersion>23</SupportedOSPlatformVersion>
7+
<Nullable>enable</Nullable>
8+
<ImplicitUsings>enable</ImplicitUsings>
9+
<LangVersion>latest</LangVersion>
10+
<ApplicationId>com.generalupdate.mauiupdate</ApplicationId>
11+
<ApplicationVersion>1</ApplicationVersion>
12+
<ApplicationDisplayVersion>1.0.0.0</ApplicationDisplayVersion>
13+
<ApplicationTitle>MauiUpdate</ApplicationTitle>
14+
<AndroidPackageFormat>apk</AndroidPackageFormat>
15+
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
16+
<UseMaui>true</UseMaui>
17+
<SingleProject>true</SingleProject>
18+
<RootNamespace>MauiUpdate</RootNamespace>
19+
<AssemblyName>MauiUpdate</AssemblyName>
20+
<!-- Preserve types for JSON deserialization -->
21+
<TrimmerSingleWarn>false</TrimmerSingleWarn>
22+
</PropertyGroup>
23+
24+
<ItemGroup>
25+
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.20" />
26+
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="10.0.20" />
27+
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
28+
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
29+
</ItemGroup>
30+
31+
<!-- Reference local GeneralUpdate.Maui.Android NuGet -->
32+
<ItemGroup>
33+
<PackageReference Include="GeneralUpdate.Maui.Android" Version="1.0.1-auth" />
34+
</ItemGroup>
35+
36+
<ItemGroup>
37+
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appicon.svg" />
38+
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#6200EE" BaseSize="128,128" />
39+
</ItemGroup>
40+
41+
<ItemGroup>
42+
<AndroidResource Include="Platforms\Android\Resources\xml\file_paths.xml" />
43+
<AndroidResource Include="Platforms\Android\Resources\xml\network_security_config.xml" />
44+
</ItemGroup>
45+
46+
</Project>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using GeneralUpdate.Maui.Android.Enums;
2+
3+
namespace MauiUpdate.Models;
4+
5+
/// <summary>
6+
/// Update package info returned by the server version API.
7+
/// Manually parsed from JSON to avoid AOT/trimming issues.
8+
/// </summary>
9+
public sealed class UpdatePackageDto
10+
{
11+
public string PacketName { get; set; } = string.Empty;
12+
public string Hash { get; set; } = string.Empty;
13+
public string Version { get; set; } = string.Empty;
14+
public string PubTime { get; set; } = string.Empty;
15+
public int AppType { get; set; }
16+
public int Platform { get; set; }
17+
public string ProductId { get; set; } = string.Empty;
18+
public bool IsForcibly { get; set; }
19+
public string Format { get; set; } = ".apk";
20+
public long Size { get; set; }
21+
public string DownloadUrl { get; set; } = string.Empty;
22+
23+
// --- Per-package authentication fields ---
24+
// Set these when the server requires authentication for update downloads.
25+
26+
/// <summary>Authentication scheme (Bearer, ApiKey, Basic, Hmac).</summary>
27+
public AuthScheme? AuthScheme { get; set; }
28+
29+
/// <summary>Token/key for Bearer or ApiKey authentication.</summary>
30+
public string? AuthToken { get; set; }
31+
32+
/// <summary>Secret key for HMAC-SHA256 authentication.</summary>
33+
public string? AuthSecretKey { get; set; }
34+
35+
/// <summary>Username for Basic authentication.</summary>
36+
public string? BasicUsername { get; set; }
37+
38+
/// <summary>Password for Basic authentication.</summary>
39+
public string? BasicPassword { get; set; }
40+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
<application
4+
android:allowBackup="true"
5+
android:icon="@mipmap/appicon"
6+
android:roundIcon="@mipmap/appicon_round"
7+
android:supportsRtl="true"
8+
android:networkSecurityConfig="@xml/network_security_config">
9+
10+
<provider
11+
android:name="androidx.core.content.FileProvider"
12+
android:authorities="com.generalupdate.mauiupdate.fileprovider"
13+
android:exported="false"
14+
android:grantUriPermissions="true">
15+
<meta-data
16+
android:name="android.support.FILE_PROVIDER_PATHS"
17+
android:resource="@xml/file_paths" />
18+
</provider>
19+
</application>
20+
21+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
22+
<uses-permission android:name="android.permission.INTERNET" />
23+
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
24+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
25+
android:maxSdkVersion="28" />
26+
</manifest>

0 commit comments

Comments
 (0)