Skip to content

Commit 96a89b0

Browse files
JusterZhuclaude
andauthored
feat: add Android APK update sample with GeneralUpdate.Avalonia (#118)
* 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> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 480c2d8 commit 96a89b0

29 files changed

Lines changed: 1108 additions & 99 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<Solution>
2+
<Project Path="src/AndroidUpdate.Android/AndroidUpdate.Android.csproj" />
3+
</Solution>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Android.App;
2+
using Android.Runtime;
3+
using AndroidUpdate.ViewModels;
4+
using Avalonia;
5+
using Avalonia.Android;
6+
7+
namespace AndroidUpdate.Android;
8+
9+
[global::Android.App.Application]
10+
public class AndroidApp : AvaloniaAndroidApplication<App>
11+
{
12+
protected AndroidApp(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
13+
{
14+
// Read real app version from package manager as early as possible
15+
try
16+
{
17+
var pkgInfo = PackageManager?.GetPackageInfo(PackageName!, 0);
18+
if (pkgInfo?.VersionName != null)
19+
App.DeviceVersion = pkgInfo.VersionName;
20+
}
21+
catch { /* fallback to default "1.0.0.0" */ }
22+
}
23+
24+
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
25+
{
26+
// Register the Android-specific handler factory before Avalonia starts
27+
App.HandlerFactory = (packageInfo, currentVersion) =>
28+
new Services.AndroidUpdateHandler(packageInfo, currentVersion);
29+
30+
return base.CustomizeAppBuilder(builder)
31+
.WithInterFont();
32+
}
33+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net10.0-android</TargetFramework>
5+
<SupportedOSPlatformVersion>23</SupportedOSPlatformVersion>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<LangVersion>latest</LangVersion>
9+
<ApplicationId>com.generalupdate.androidupdate</ApplicationId>
10+
<ApplicationVersion>7</ApplicationVersion>
11+
<ApplicationDisplayVersion>1.0.0.0</ApplicationDisplayVersion>
12+
<ApplicationTitle>AndroidUpdate</ApplicationTitle>
13+
<AndroidPackageFormat>apk</AndroidPackageFormat>
14+
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
15+
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
16+
</PropertyGroup>
17+
18+
<ItemGroup>
19+
<PackageReference Include="Avalonia" Version="12.0.3" />
20+
<PackageReference Include="Avalonia.Android" Version="12.0.3" />
21+
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.3" />
22+
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.3" />
23+
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1">
24+
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
25+
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
26+
</PackageReference>
27+
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
28+
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.15" />
29+
<AndroidResource Include="Icon.png">
30+
<Link>Resources\drawable\Icon.png</Link>
31+
</AndroidResource>
32+
<AndroidResource Include="Resources\xml\file_paths.xml" />
33+
<AndroidResource Include="Resources\xml\network_security_config.xml" />
34+
</ItemGroup>
35+
36+
<!-- Reference the compiled GeneralUpdate.Avalonia.Android DLL -->
37+
<ItemGroup>
38+
<Reference Include="GeneralUpdate.Avalonia.Android">
39+
<HintPath>libs\GeneralUpdate.Avalonia.Android.dll</HintPath>
40+
</Reference>
41+
</ItemGroup>
42+
</Project>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Application xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:local="using:AndroidUpdate"
4+
x:Class="AndroidUpdate.App"
5+
RequestedThemeVariant="Default">
6+
7+
<Application.DataTemplates>
8+
<local:ViewLocator/>
9+
</Application.DataTemplates>
10+
11+
<Application.Styles>
12+
<FluentTheme />
13+
</Application.Styles>
14+
</Application>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using System.Net.Http;
3+
using AndroidUpdate.ViewModels;
4+
using Avalonia;
5+
using Avalonia.Controls.ApplicationLifetimes;
6+
using Avalonia.Markup.Xaml;
7+
8+
namespace AndroidUpdate;
9+
10+
public partial class App : Avalonia.Application
11+
{
12+
/// <summary>
13+
/// Static factory for creating platform-specific update handlers.
14+
/// Set by the Android project (or other platform projects) during startup.
15+
/// </summary>
16+
public static Func<UpdatePackageDto, string, IAndroidUpdateHandler>? HandlerFactory { get; set; }
17+
18+
/// <summary>
19+
/// The device's currently installed app version.
20+
/// Set by the platform project on startup; falls back to "1.0.0.0".
21+
/// </summary>
22+
public static string DeviceVersion { get; set; } = "1.0.0.0";
23+
24+
public override void Initialize()
25+
{
26+
AvaloniaXamlLoader.Load(this);
27+
}
28+
29+
public override void OnFrameworkInitializationCompleted()
30+
{
31+
var httpClient = new HttpClient();
32+
httpClient.Timeout = TimeSpan.FromSeconds(30);
33+
34+
MainViewViewModel CreateViewModel() =>
35+
new(httpClient, (pkg, ver) =>
36+
{
37+
if (HandlerFactory == null)
38+
throw new InvalidOperationException(
39+
"HandlerFactory not set. Ensure the platform project initializes it.");
40+
return HandlerFactory(pkg, ver);
41+
});
42+
43+
if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactory)
44+
{
45+
// Android: use MainViewFactory for Activity-based lifetime
46+
singleViewFactory.MainViewFactory = () =>
47+
new Views.MainView { DataContext = CreateViewModel() };
48+
}
49+
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
50+
{
51+
singleViewPlatform.MainView = new Views.MainView
52+
{
53+
DataContext = CreateViewModel()
54+
};
55+
}
56+
57+
base.OnFrameworkInitializationCompleted();
58+
}
59+
}
11.5 KB
Loading
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Android.App;
2+
using Android.Content.PM;
3+
using Avalonia;
4+
using Avalonia.Android;
5+
6+
namespace AndroidUpdate.Android;
7+
8+
[Activity(
9+
Label = "AndroidUpdate",
10+
Theme = "@style/MyTheme.NoActionBar",
11+
MainLauncher = true,
12+
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
13+
public class MainActivity : AvaloniaMainActivity
14+
{
15+
}
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+
package="com.generalupdate.androidupdate"
4+
android:versionCode="7"
5+
android:versionName="1.0.0.0">
6+
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="36" />
7+
<application android:label="AndroidUpdate"
8+
android:theme="@style/MyTheme.NoActionBar"
9+
android:networkSecurityConfig="@xml/network_security_config">
10+
<!-- FileProvider for APK installation -->
11+
<provider android:name="androidx.core.content.FileProvider"
12+
android:authorities="com.generalupdate.androidupdate.fileprovider"
13+
android:exported="false"
14+
android:grantUriPermissions="true">
15+
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
16+
android:resource="@xml/file_paths" />
17+
</provider>
18+
</application>
19+
<!-- Required for APK installation on Android 8+ -->
20+
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
21+
<!-- Internet access for downloading updates -->
22+
<uses-permission android:name="android.permission.INTERNET" />
23+
<!-- Write external storage for download directory -->
24+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
25+
android:maxSdkVersion="28" />
26+
</manifest>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<animated-vector
2+
xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:aapt="http://schemas.android.com/aapt">
4+
<aapt:attr name="android:drawable">
5+
<vector
6+
android:name="vector"
7+
android:width="128dp"
8+
android:height="128dp"
9+
android:viewportWidth="128"
10+
android:viewportHeight="128">
11+
<group
12+
android:name="wrapper"
13+
android:translateX="21"
14+
android:translateY="21">
15+
<group android:name="group">
16+
<path
17+
android:name="path"
18+
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
19+
android:strokeWidth="1"/>
20+
<path
21+
android:name="path_1"
22+
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
23+
android:strokeWidth="1"
24+
android:fillType="evenOdd"/>
25+
<path
26+
android:name="path_2"
27+
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
28+
android:strokeWidth="1"/>
29+
</group>
30+
</group>
31+
</vector>
32+
</aapt:attr>
33+
<target android:name="path">
34+
<aapt:attr name="android:animation">
35+
<objectAnimator
36+
android:propertyName="fillColor"
37+
android:duration="1000"
38+
android:valueFrom="#00ffffff"
39+
android:valueTo="#161c2d"
40+
android:valueType="colorType"
41+
android:interpolator="@android:interpolator/fast_out_slow_in"/>
42+
</aapt:attr>
43+
</target>
44+
<target android:name="path_1">
45+
<aapt:attr name="android:animation">
46+
<objectAnimator
47+
android:propertyName="fillColor"
48+
android:duration="1000"
49+
android:valueFrom="#00ffffff"
50+
android:valueTo="#f9f9fb"
51+
android:valueType="colorType"
52+
android:interpolator="@android:interpolator/fast_out_slow_in"/>
53+
</aapt:attr>
54+
</target>
55+
<target android:name="path_2">
56+
<aapt:attr name="android:animation">
57+
<objectAnimator
58+
android:propertyName="fillColor"
59+
android:duration="1000"
60+
android:valueFrom="#00ffffff"
61+
android:valueTo="#f9f9fb"
62+
android:valueType="colorType"
63+
android:interpolator="@android:interpolator/fast_out_slow_in"/>
64+
</aapt:attr>
65+
</target>
66+
</animated-vector>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<animated-vector
2+
xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:aapt="http://schemas.android.com/aapt">
4+
<aapt:attr name="android:drawable">
5+
<vector
6+
android:name="vector"
7+
android:width="128dp"
8+
android:height="128dp"
9+
android:viewportWidth="128"
10+
android:viewportHeight="128">
11+
<group
12+
android:name="wrapper"
13+
android:translateX="21"
14+
android:translateY="21">
15+
<group android:name="group">
16+
<path
17+
android:name="path"
18+
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
19+
android:fillColor="#00ffffff"
20+
android:strokeWidth="1"/>
21+
<path
22+
android:name="path_1"
23+
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
24+
android:fillColor="#00ffffff"
25+
android:strokeWidth="1"
26+
android:fillType="evenOdd"/>
27+
<path
28+
android:name="path_2"
29+
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
30+
android:fillColor="#00ffffff"
31+
android:strokeWidth="1"/>
32+
</group>
33+
</group>
34+
</vector>
35+
</aapt:attr>
36+
<target android:name="path_2">
37+
<aapt:attr name="android:animation">
38+
<objectAnimator
39+
android:propertyName="fillColor"
40+
android:startOffset="100"
41+
android:duration="900"
42+
android:valueFrom="#00ffffff"
43+
android:valueTo="#161c2d"
44+
android:valueType="colorType"
45+
android:interpolator="@android:interpolator/fast_out_slow_in"/>
46+
</aapt:attr>
47+
</target>
48+
<target android:name="path">
49+
<aapt:attr name="android:animation">
50+
<objectAnimator
51+
android:propertyName="fillColor"
52+
android:duration="500"
53+
android:valueFrom="#00ffffff"
54+
android:valueTo="#f9f9fb"
55+
android:valueType="colorType"
56+
android:interpolator="@android:interpolator/fast_out_slow_in"/>
57+
</aapt:attr>
58+
</target>
59+
<target android:name="path_1">
60+
<aapt:attr name="android:animation">
61+
<objectAnimator
62+
android:propertyName="fillColor"
63+
android:startOffset="100"
64+
android:duration="900"
65+
android:valueFrom="#00ffffff"
66+
android:valueTo="#161c2d"
67+
android:valueType="colorType"
68+
android:interpolator="@android:interpolator/fast_out_slow_in"/>
69+
</aapt:attr>
70+
</target>
71+
</animated-vector>

0 commit comments

Comments
 (0)