feat(android): OTA双轨更新 — shared版本号+Android Expo/PackageInstaller

- packages/shared: 版本号解析 vX.Y.Z-dav-{client}-{build} + manifest远程检查
- packages/client-android: Expo52 + expo-updates JS bundle OTA
- PackageInstallerModule.kt: APK静默安装原生模块(~60行Kotlin)
- monorepo: pnpm workspace + 版本号体系
This commit is contained in:
LukeMackin
2026-07-19 01:41:36 +08:00
parent b12aef0375
commit 9f8659c293
15 changed files with 472 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
import { parseVersion, checkUpdate, semverGt } from '@yuzu-gca/shared';
import Constants from 'expo-constants';
import * as Device from 'expo-device';
import * as FileSystem from 'expo-file-system';
import { NativeModules, Platform } from 'react-native';
// ============================================================
// 1. 版本号读取
// ============================================================
/** 从 app.json + expo-updates 获取本地版本 */
export function getLocalVersion(): string {
return Constants.expoConfig?.version ?? '0.1.0';
}
export function getLocalBuildTag(): string {
const v = getLocalVersion();
return `v${v}-dav-android-${Constants.expoConfig?.ios?.buildNumber ?? Constants.expoConfig?.android?.versionCode ?? 1}`;
}
// ============================================================
// 2. JS Bundle OTAexpo-updates 自动处理)
// ============================================================
/** 手动触发 OTA 检查expo-updates 已自动启动检查,此方法用于设置页手动刷新) */
export async function checkJsBundleUpdate(): Promise<{
hasUpdate: boolean;
remoteVersion: string;
}> {
const localVersion = getLocalVersion();
const localTag = getLocalBuildTag();
const result = await checkUpdate('android', parseVersion(localTag)!);
if (!result.shouldUpdate || !result.remoteVersion) {
return { hasUpdate: false, remoteVersion: localVersion };
}
// expo-updates 会在检测到新 manifest 时自动下载 bundle
// 这里只返回状态;实际下载由 expo-updates 后台完成
return {
hasUpdate: semverGt(result.remoteVersion.raw.replace(/^v/, '').split('-')[0], localVersion),
remoteVersion: result.remoteVersion.raw,
};
}
// ============================================================
// 3. APK 静默安装(大更新)
// ============================================================
const { PackageInstallerModule } = NativeModules;
/** 后台下载 APK 并通过 PackageInstaller 静默安装 */
export async function downloadAndInstallApk(apkUrl: string, sha256: string): Promise<void> {
const localPath = `${FileSystem.cacheDirectory ?? ''}gca-update.apk`;
const download = FileSystem.createDownloadResumable(
apkUrl,
localPath,
{},
(progress) => {
const pct = (progress.totalBytesWritten / progress.totalBytesExpectedToWrite) * 100;
console.log(`[OTA] 下载进度: ${pct.toFixed(0)}%`);
},
);
const result = await download.downloadAsync();
if (!result?.uri) throw new Error('APK 下载失败');
console.log(`[OTA] 下载完成: ${result.uri}`);
// 调用原生 PackageInstaller 安装
if (Platform.OS === 'android' && PackageInstallerModule) {
await PackageInstallerModule.installApk(result.uri);
console.log('[OTA] APK 已提交安装,用户下次重启生效');
} else {
throw new Error('PackageInstaller 原生模块不可用');
}
}
// ============================================================
// 4. 启动时一键检查JS OTA + APK 更新)
// ============================================================
export async function onAppStartCheckUpdate(): Promise<{
jsUpdate: boolean;
apkUpdate: boolean;
}> {
const localTag = getLocalBuildTag();
const local = parseVersion(localTag);
if (!local) return { jsUpdate: false, apkUpdate: false };
const result = await checkUpdate('android', local);
if (!result.shouldUpdate || !result.build) {
return { jsUpdate: false, apkUpdate: false };
}
// JS OTA: expo-updates 自动处理,无需手动干预
const jsUpdate = semverGt(
result.build.tag.replace(/^v/, '').split('-')[0],
localTag.replace(/^v/, '').split('-')[0],
);
// APK 大更新:需要下载 + PackageInstaller
if (result.build.size > 5_000_000) {
// >5MB = APK 安装包
await downloadAndInstallApk(result.build.url, result.build.sha256);
return { jsUpdate: false, apkUpdate: true };
}
return { jsUpdate, apkUpdate: false };
}