From 05b6bd7ffc515ce460d969d97b8d827c1a62721e Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Thu, 11 Jun 2026 16:58:13 +0800 Subject: [PATCH 1/6] 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 --- ...epSeek-V4-Pro-\345\256\236\346\210\230.md" | 94 +++++++++++++++++++ src/Server/DTOs/VerificationResultDTO.cs | 5 - src/Server/DTOs/VerifyDTO.cs | 4 - src/Server/Program.cs | 15 +-- 4 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 "src/Hub/Samples/VS-Code-Claude-Code-DeepSeek-V4-Pro-\345\256\236\346\210\230.md" diff --git "a/src/Hub/Samples/VS-Code-Claude-Code-DeepSeek-V4-Pro-\345\256\236\346\210\230.md" "b/src/Hub/Samples/VS-Code-Claude-Code-DeepSeek-V4-Pro-\345\256\236\346\210\230.md" new file mode 100644 index 0000000..debf79a --- /dev/null +++ "b/src/Hub/Samples/VS-Code-Claude-Code-DeepSeek-V4-Pro-\345\256\236\346\210\230.md" @@ -0,0 +1,94 @@ +# 用 VS Code Claude Code + DeepSeek V4 Pro 写代码的一些记录 + +## 一、背景 + +Claude Code 的 VS Code 插件出来之后我一直想试试,但这东西默认走 Anthropic 自己的 API,Opus 4.6 输出价格 $25/百万 token,高强度用一个月账单不太好看。后来发现 DeepSeek 官方提供了兼容 Anthropic 协议的 API 端点,输出价格 $0.87/百万 token,差了一个数量级。于是就搭起来用了段时间,这篇文章是把整个过程和感受记下来,给想试的人一个参考。 + +--- + +## 二、环境搭建 + +### 2.1 装插件 + +VS Code 里按 `Ctrl+Shift+X` 搜 "Claude Code",发布者是 Anthropic,点安装就行。装完右上角会出现一个闪电图标,那是打开 Claude Code 面板的入口。 + +没什么特别的,跟装其他扩展一样。 + +### 2.2 配置 DeepSeek 的 API 密钥 + +去 [platform.deepseek.com](https://platform.deepseek.com) 注册账号,在 API Keys 页面创建一个 Key。注意关掉弹窗就看不到了,提前存好。 + +然后在 VS Code 里按 `Ctrl+Shift+P`,打开 `Open User Settings (JSON)`,加这段配置: + +```json +"claudeCode.environmentVariables": [ + { "name": "ANTHROPIC_BASE_URL", "value": "https://api.deepseek.com/anthropic" }, + { "name": "ANTHROPIC_AUTH_TOKEN", "value": "sk-你的DeepSeek_API_Key" }, + { "name": "ANTHROPIC_MODEL", "value": "deepseek-v4-pro[1m]" }, + { "name": "ANTHROPIC_DEFAULT_OPUS_MODEL", "value": "deepseek-v4-pro[1m]" }, + { "name": "ANTHROPIC_DEFAULT_SONNET_MODEL", "value": "deepseek-v4-pro[1m]" }, + { "name": "ANTHROPIC_DEFAULT_HAIKU_MODEL", "value": "deepseek-v4-flash" }, + { "name": "CLAUDE_CODE_SUBAGENT_MODEL", "value": "deepseek-v4-flash" }, + { "name": "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "value": "1" }, + { "name": "CLAUDE_CODE_EFFORT_LEVEL", "value": "max" }, + { "name": "API_TIMEOUT_MS", "value": "600000" } +] +``` + +说几个注意点: + +- **`[1m]` 后缀加上**,否则默认上下文只有 64K,Claude Code 跑 agent 模式几个来回就超了。 +- **SUBAGENT_MODEL 我设成了 Flash**,文件搜索、索引这些辅助任务没必要上 Pro,省点钱。 +- **`DISABLE_NONESSENTIAL_TRAFFIC` 设 1**,DeepSeek 的兼容端点对某些预检请求会返回 404,设了这个直接跳过,省得报错刷屏。 + +配置完重启 VS Code,打开 Claude Code 面板输入 `/model`,返回 `deepseek-v4-pro` 就说明通了。 + +另外这些环境变量配置其实不用自己手敲——有其他 AI Agent 的话可以代劳,比如 OpenClaw 里通过 MCP 工具直接写 settings.json,一句话搞定。 + +### 2.3 实际用起来的感受 + +快捷键:`Ctrl+Shift+I` 打开面板,`Ctrl+Esc` 切换焦点,`Ctrl+N` 新建对话。 + +几个场景的感受: + +**写代码。** 需求明确的时候它干得不错。比如"写一个支持断点续传的 HTTP 上传客户端,用 asyncio,带进度回调"——生成的代码结构清晰,分片逻辑、md5 校验、重试机制都带了,基本改改就能用。 + +**重构。** 选中一段老代码让它拆函数加类型注解,它能理解意图,但有个毛病——容易过度工程化。50 行的函数它能给你拆成 5 个文件 8 个类,你得自己往回拉。 + +**Debug。** 报错堆栈丢给它定位方向还行,但深层的逻辑漏洞它经常"自圆其说",给一个看似合理但实际错误的解释。最终还是靠自己。 + +--- + +## 三、一些心得 + +### 3.1 智能程度:能干活,但得有人带着 + +SWE Verified 上 V4 Pro 是 80.6%,Opus 4.6 Max 是 80.8%,数字看不出什么差距。实际用下来,**需求明确、路径清晰的任务**——比如"实现一个 Redis 分布式锁"、"写 JWT 中间件"——它完成度很高。但需求模糊、需要自己判断决策的地方,它就容易写出看似正确但细节不对的东西。 + +我的感受是:把它当成一个效率放大器比较合适。**你自己心里得有清晰的思路,知道每一步要什么,它能帮你把想法快速落地。** 反过来,如果你自己都不知道该怎么写,指望它给你一个完整的架构设计,那大概率会失望。 + +### 3.2 测试:帮得上忙,但核心流程还是得人走 + +写单元测试、集成测试这种套路固定的工作,它做得不错。给个函数它能快速生成 edge case 覆盖、mock 依赖、assert 断言,比自己手写快不少。搭测试环境、写 Dockerfile、配 CI 这类事也基本零失误。 + +但"帮我写测试看看能不能测出 bug"——这件事它做不好。生成的测试大概率只走 happy path,隐藏的并发竞争条件、异常状态流转它发现不了。**找 bug 还是靠人的直觉和对代码的理解。** + +### 3.3 性价比:划算,但得看场景 + +价格上 DeepSeek V4 Pro 确实便宜很多:输入 $0.435、输出 $0.87(每百万 token)。Opus 4.6 是 $5 和 $25。差了将近 29 倍。 + +但这里有个反直觉的事。我之前遇到一个依赖注入容器的生命周期问题,涉及多个框架层的交互,比较复杂。用 DeepSeek V4 Pro 调了大半天,token 烧了将近 1 亿,来回改了好几轮都没解决。后来切回 Opus 4.6,20 分钟定位问题,10 分钟修完。算下来 DeepSeek 花了大概 $80 加大半天时间,Opus 花了大概 $50 加半小时。 + +**贵的模型不一定更贵。** 如果任务难度高,更强的模型虽然单价贵,但总成本(算上你的时间)反而更低。如果任务简单明确——写 CRUD、写测试、写配置——那 DeepSeek V4 Pro 确实划算。 + +我现在是**日常开发用 DeepSeek,遇到疑难杂症切回 Opus**。成本能控住,关键时刻效率也不掉。 + +--- + +## 四、总结 + +VS Code Claude Code + DeepSeek V4 Pro 这套组合,对于预算有限的个人开发者和中小团队来说是个实惠的选择。配置简单、日常编码够用、成本低。缺点是复杂推理不如顶级闭源模型、偶尔过度工程化、没有多模态能力。 + +适合的场景是"你心里有清晰的代码蓝图,需要一个人快速把代码落地"。它不是自动驾驶,是一个能提效的辅助工具。 + +**模型选型看场景,日常省钱、关键时刻舍得用好模型,互补着来比较实际。** diff --git a/src/Server/DTOs/VerificationResultDTO.cs b/src/Server/DTOs/VerificationResultDTO.cs index dfb2cac..112896b 100644 --- a/src/Server/DTOs/VerificationResultDTO.cs +++ b/src/Server/DTOs/VerificationResultDTO.cs @@ -72,11 +72,6 @@ public record VerificationResultDTO /// public bool? IsFreeze { get; set; } - /// - /// 升级模式: 1=VersionChain, 2=CrossVersion - /// - public int? UpgradeMode { get; set; } - /// /// 是否为跨版本升级包 /// diff --git a/src/Server/DTOs/VerifyDTO.cs b/src/Server/DTOs/VerifyDTO.cs index 6656707..49abbc2 100644 --- a/src/Server/DTOs/VerifyDTO.cs +++ b/src/Server/DTOs/VerifyDTO.cs @@ -27,8 +27,4 @@ public class VerifyDTO /// public string? ProductId { get; set; } - /// - /// 升级模式: 1=VersionChain(逐版本链式升级), 2=CrossVersion(跨版本升级) - /// - public int? UpgradeMode { get; set; } } \ No newline at end of file diff --git a/src/Server/Program.cs b/src/Server/Program.cs index 2bca89e..df4119d 100644 --- a/src/Server/Program.cs +++ b/src/Server/Program.cs @@ -41,14 +41,12 @@ // ── Verification handler (shared across URL variants) ────────── var handleVerification = (VerifyDTO request) => { - Console.WriteLine($"[Verification] AppKey={request.AppKey}, Version={request.Version}, AppType={request.AppType}, Platform={request.Platform}, ProductId={request.ProductId}, UpgradeMode={request.UpgradeMode}"); + Console.WriteLine($"[Verification] AppKey={request.AppKey}, Version={request.Version}, AppType={request.AppType}, Platform={request.Platform}, ProductId={request.ProductId}"); if (!Version.TryParse(request.Version ?? "0.0.0.0", out var currentVersion)) return Results.Ok(HttpResponseDTO>.Success( Array.Empty(), "Invalid version format.")); - var requestUpgradeMode = request.UpgradeMode ?? 1; - var available = versionStore .Where(v => { @@ -59,13 +57,6 @@ if (!string.IsNullOrEmpty(request.ProductId) && !string.IsNullOrEmpty(v.ProductId) && !string.Equals(v.ProductId, request.ProductId, StringComparison.OrdinalIgnoreCase)) return false; - if (requestUpgradeMode == 1 && v.IsCrossVersion == true) return false; - if (requestUpgradeMode == 2) - { - if (v.IsCrossVersion != true) return false; - if (!string.Equals(v.FromVersion, request.Version, StringComparison.OrdinalIgnoreCase)) - return false; - } return true; }) .OrderByDescending(v => new Version(v.Version!)) @@ -89,14 +80,12 @@ Version = v.Version, AppType = v.AppType, Platform = v.Platform, ProductId = v.ProductId, IsForcibly = v.IsForcibly, Format = v.Format ?? ".zip", Size = v.Size, IsFreeze = v.IsFreeze, - UpgradeMode = requestUpgradeMode, IsCrossVersion = v.IsCrossVersion ?? false, FromVersion = v.FromVersion }); } - var modeLabel = requestUpgradeMode == 2 ? "CrossVersion" : "VersionChain"; - Console.WriteLine($"[Verification] Returning {results.Count} packages (Mode: {modeLabel})"); + Console.WriteLine($"[Verification] Returning {results.Count} packages"); foreach (var r in results) Console.WriteLine($" {r.Version} — {r.Name} ({(r.IsCrossVersion == true ? $"Cross {r.FromVersion} → {r.Version}" : "Full")})"); From c028beb093b1606aaa9bc7aaeb62392aaa934e22 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sat, 13 Jun 2026 15:44:55 +0800 Subject: [PATCH 2/6] 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 --- UI/AndroidUpdate/AndroidUpdate.slnx | 3 + .../src/AndroidUpdate.Android/AndroidApp.cs | 33 +++ .../AndroidUpdate.Android.csproj | 42 +++ .../src/AndroidUpdate.Android/App.axaml | 14 + .../src/AndroidUpdate.Android/App.axaml.cs | 59 ++++ .../src/AndroidUpdate.Android/Icon.png | Bin 0 -> 11731 bytes .../src/AndroidUpdate.Android/MainActivity.cs | 15 + .../Properties/AndroidManifest.xml | 26 ++ .../drawable-night-v31/avalonia_anim.xml | 66 +++++ .../Resources/drawable-v31/avalonia_anim.xml | 71 +++++ .../Resources/drawable/splash_screen.xml | 13 + .../Resources/values-night/colors.xml | 4 + .../Resources/values-v31/styles.xml | 21 ++ .../Resources/values/colors.xml | 4 + .../Resources/values/styles.xml | 12 + .../Resources/xml/file_paths.xml | 8 + .../Resources/xml/network_security_config.xml | 7 + .../Services/AndroidUpdateHandler.cs | 118 ++++++++ .../src/AndroidUpdate.Android/ViewLocator.cs | 34 +++ .../ViewModels/IAndroidUpdateHandler.cs | 13 + .../ViewModels/MainViewViewModel.cs | 262 ++++++++++++++++++ .../ViewModels/UpdatePackageInfo.cs | 12 + .../ViewModels/ViewModelBase.cs | 7 + .../Views/MainView.axaml | 129 +++++++++ .../Views/MainView.axaml.cs | 11 + .../libs/GeneralUpdate.Avalonia.Android.dll | Bin 0 -> 60928 bytes src/Server/Program.cs | 17 +- src/Server/appsettings.json | 4 +- src/Server/wwwroot/packages/versions.json | 193 +++++++------ 29 files changed, 1099 insertions(+), 99 deletions(-) create mode 100644 UI/AndroidUpdate/AndroidUpdate.slnx create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/AndroidApp.cs create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/AndroidUpdate.Android.csproj create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/App.axaml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/App.axaml.cs create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Icon.png create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/MainActivity.cs create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Properties/AndroidManifest.xml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable-night-v31/avalonia_anim.xml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable-v31/avalonia_anim.xml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/drawable/splash_screen.xml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values-night/colors.xml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values-v31/styles.xml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values/colors.xml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/values/styles.xml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/xml/file_paths.xml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/xml/network_security_config.xml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Services/AndroidUpdateHandler.cs create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/ViewLocator.cs create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/IAndroidUpdateHandler.cs create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/MainViewViewModel.cs create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/UpdatePackageInfo.cs create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/ViewModelBase.cs create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Views/MainView.axaml create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/Views/MainView.axaml.cs create mode 100644 UI/AndroidUpdate/src/AndroidUpdate.Android/libs/GeneralUpdate.Avalonia.Android.dll 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..1a4bb98 --- /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 0000000000000000000000000000000000000000..3c39845930eae5a98e094bdada0da2d6646bf9ce GIT binary patch literal 11731 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 literal 0 HcmV?d00001 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..9c3ff2a --- /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..7a6eba3 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/Resources/xml/file_paths.xml @@ -0,0 +1,8 @@ + + + + + + + + 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..9122fd3 --- /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 PackageInfo _packageInfo; + private bool _disposed; + + public event EventHandler? ProgressChanged; + public event EventHandler? StatusChanged; + + public AndroidUpdateHandler(PackageInfo 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..0489d84 --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/MainViewViewModel.cs @@ -0,0 +1,262 @@ +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 PackageInfo? _pendingUpdate; + + public MainViewViewModel(HttpClient httpClient, Func handlerFactory) + { + _httpClient = httpClient; + _handlerFactory = handlerFactory; + } + + // ── 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(); + 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; + StatusText = $"Updated to v{CurrentVersion}!"; + DownloadProgress = 100; + ProgressText = "Done"; + } + } + catch (OperationCanceledException) + { + StatusText = "Cancelled."; + } + finally + { + IsDownloading = false; + _cts?.Dispose(); + _cts = null; + _currentHandler?.Dispose(); + _currentHandler = null; + } + } + + // ── 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 PackageInfo( + 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() + ); + } + + private static string ServerBaseUrl => "http://192.168.50.204:5000"; +} diff --git a/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/UpdatePackageInfo.cs b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/UpdatePackageInfo.cs new file mode 100644 index 0000000..a17fb4b --- /dev/null +++ b/UI/AndroidUpdate/src/AndroidUpdate.Android/ViewModels/UpdatePackageInfo.cs @@ -0,0 +1,12 @@ +namespace AndroidUpdate.ViewModels; + +/// +/// Describes an update package returned by the server's verification endpoint. +/// +public sealed record PackageInfo( + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +