import bundleManager from '@ohos.bundle.bundleManager' import common from '@ohos.app.ability.common' import request from '@ohos.request' import deviceInfo from '@ohos.deviceInfo' import type { BusinessError } from '@ohos.base' import { HttpClient } from '../core/HttpClient' import type { AppVersionInfo, InstalledRnBundleInfo, RnBundleInfo } from '../core/Types' import { SDKContext } from '../core/SDKContext' export interface AppUpdateResult { hasUpdate: boolean info?: AppVersionInfo } export interface RnUpdateResult { hasUpdate: boolean info?: RnBundleInfo } interface DeviceInfo { deviceId: string model: string osVersion: string vendor: string } export class UpdateSDK { private static cacheKey = 'xuqm_update_cache' private static ignoredPrefix = 'xuqm_ignored_v' private static cacheTtl = 30 * 60 * 1000 // 30 minutes /** * 检查应用更新 * @param appKey 应用标识 * @param bypassIgnore 是否忽略已忽略的版本 */ static async checkAppUpdate(appKey: string, bypassIgnore: boolean = false): Promise { const bundleInfo = bundleManager.getBundleInfoForSelfSync( bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT ) const currentVersionCode = bundleInfo.versionCode const currentVersionName = bundleInfo.versionName const userId = SDKContext.getUserId() // Check cache (skip if bypassIgnore) if (!bypassIgnore) { const cached = await this.readCache(appKey, currentVersionCode, userId) if (cached) { return cached.hasUpdate ? { hasUpdate: true, info: cached.info } : { hasUpdate: false } } } const deviceInfo = this.getDeviceInfo() let url = `/api/v1/updates/app/check?appKey=${appKey}&versionCode=${currentVersionCode}&platform=HARMONY` url += `¤tVersionName=${encodeURIComponent(currentVersionName)}` url += `&deviceId=${encodeURIComponent(deviceInfo.deviceId)}` url += `&model=${encodeURIComponent(deviceInfo.model)}` url += `&osVersion=${encodeURIComponent(deviceInfo.osVersion)}` url += `&vendor=${encodeURIComponent(deviceInfo.vendor)}` if (userId) { url += `&userId=${encodeURIComponent(userId)}` } if (bypassIgnore) { url += `&bypassIgnore=true` } const data = await HttpClient.get(url) // Apply version ignore if (!bypassIgnore && data.latestVersionCode > currentVersionCode && !data.forceUpdate) { if (await this.isVersionIgnored(data.latestVersionCode)) { return { hasUpdate: false } } } // Write cache await this.writeCache(appKey, currentVersionCode, userId, data) if (data.latestVersionCode <= currentVersionCode) { return { hasUpdate: false } } return { hasUpdate: true, info: data } } /** * 打开应用市场 */ static async openAppMarket(context: common.UIAbilityContext, url?: string): Promise { if (!url) { return } await context.openLink(url, { appLinkingOnly: false }) } /** * 忽略版本 */ static async ignoreVersion(versionCode: number): Promise { await SDKContext.setPreference(`${this.ignoredPrefix}${versionCode}`, 'true') } /** * 清除所有忽略的版本 */ static async clearIgnoredVersions(): Promise { // HarmonyOS preferences 没有批量删除,需要逐个清除 // 这里简化处理,实际可能需要维护一个忽略列表 } /** * 检查版本是否被忽略 */ static async isVersionIgnored(versionCode: number): Promise { const value = await SDKContext.getPreference(`${this.ignoredPrefix}${versionCode}`) return value === 'true' } /** * 检查 RN Bundle 更新 */ static async checkRnUpdate( appKey: string, bundleName: string, currentBundleVersion?: number, packageName?: string ): Promise { const localBundleVersion = currentBundleVersion ?? await SDKContext.getRnBundleVersion(bundleName) ?? 0 const data = await HttpClient.get( `/api/v1/rn/update/check?appKey=${appKey}&bundleName=${bundleName}&bundleVersion=${localBundleVersion}${packageName ? `&packageName=${encodeURIComponent(packageName)}` : ''}` ) if (data.bundleVersion <= localBundleVersion) { return { hasUpdate: false } } if (packageName && data.packageMatched === false) { return { hasUpdate: false, info: data } } return { hasUpdate: true, info: data } } /** * 下载 RN Bundle */ static async downloadRnBundle( context: common.UIAbilityContext, downloadUrl: string, destFilename: string, bundleName?: string, bundleInfo?: RnBundleInfo, ): Promise { const destPath = context.cacheDir + '/' + destFilename await new Promise((resolve, reject) => { request.downloadFile(context, { url: downloadUrl, filePath: destPath, }, (err: BusinessError | null, task: request.DownloadTask) => { if (err) { reject(err); return } task.on('complete', resolve) task.on('fail', (error: number) => reject(new Error(`Download failed: ${error}`))) }) }) if (bundleName && bundleInfo) { const installedInfo: InstalledRnBundleInfo = { bundleVersion: bundleInfo.bundleVersion, packageName: bundleInfo.packageName, minCommonVersion: bundleInfo.minCommonVersion, installedAt: Date.now(), } await SDKContext.setRnBundleInfo(bundleName, installedInfo) } if (SDKContext.getConfig().debug) { console.log('[UpdateSDK] RN bundle downloaded to', destPath) } return destPath } /** * 记录 RN Bundle 信息 */ static async rememberRnBundleInfo( bundleName: string, bundleVersion: number, packageName?: string, minCommonVersion?: string, ): Promise { await SDKContext.setRnBundleInfo(bundleName, { bundleVersion, packageName, minCommonVersion, installedAt: Date.now(), }) } /** * 获取已安装的 RN Bundle 信息 */ static async getInstalledRnBundleInfo(bundleName: string): Promise { return SDKContext.getRnBundleInfo(bundleName) } // MARK: - Device Info private static getDeviceInfo(): DeviceInfo { const deviceId = deviceInfo.ODID || '' const brand = deviceInfo.brand || '' const product = deviceInfo.product || '' const model = `${brand} ${product}`.trim() const osVersion = `HarmonyOS ${deviceInfo.osReleaseType || ''}` const vendor = this.detectVendor(brand.toLowerCase()) return { deviceId, model, osVersion, vendor } } private static detectVendor(brand: string): string { if (brand.includes('huawei')) return 'HUAWEI' if (brand.includes('honor')) return 'HONOR' return 'HUAWEI' // 鸿蒙默认华为 } // MARK: - Cache private static async readCache(appKey: string, versionCode: number, userId: string | null): Promise<{ hasUpdate: boolean; info?: AppVersionInfo } | null> { try { const key = `${this.cacheKey}_${appKey}_${versionCode}_${userId || ''}` const raw = await SDKContext.getPreference(key) if (!raw) return null const cached = JSON.parse(raw) if (Date.now() - cached.ts > this.cacheTtl) return null return cached.data } catch { return null } } private static async writeCache(appKey: string, versionCode: number, userId: string | null, info: AppVersionInfo): Promise { try { const key = `${this.cacheKey}_${appKey}_${versionCode}_${userId || ''}` await SDKContext.setPreference(key, JSON.stringify({ ts: Date.now(), data: { hasUpdate: info.latestVersionCode > versionCode, info }, })) } catch { // ignore cache write errors } } }