Files
frontend/src/services/apkUpdateService.ts
lan 4b89b50006
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 3m4s
Frontend CI / ota-android (push) Successful in 11m9s
Frontend CI / build-android-apk (push) Successful in 1h23m57s
refactor(stores): unify data sources pattern and extract BaseManager base class
- Add BaseManager/CachedManager base classes to eliminate duplicate cache logic
- Add postListSources.ts with IPostListPagedSource interface
- Add groupListSources.ts with IGroupListPagedSource interface
- Refactor postManager to use CachedManager and Sources pattern
- Refactor groupManager to use CachedManager and Sources pattern
- Clean up duplicate methods in groupService.ts
- Fix TypeScript errors and remove mock data
2026-03-26 16:46:22 +08:00

291 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* APK 更新检查服务
* 用于检查最新版本并提示用户下载更新
*/
import { Platform, Linking, Alert } from 'react-native';
import Constants from 'expo-constants';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { showConfirm } from './dialogService';
// 条件导入原生模块(仅在 Android 上可用)
// eslint-disable-next-line @typescript-eslint/no-var-requires
const FileSystem = Platform.OS === 'android' ? require('expo-file-system/legacy') : null;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const IntentLauncher = Platform.OS === 'android' ? require('expo-intent-launcher') : null;
// 更新服务器地址
const UPDATES_SERVER_URL = Constants.expoConfig?.extra?.updatesUrl
? Constants.expoConfig.extra.updatesUrl.replace('/api/manifest', '')
: 'https://updates.littlelan.cn';
// 检查间隔毫秒24小时
const CHECK_INTERVAL = 24 * 60 * 60 * 1000;
// 最后检查时间存储键
const LAST_CHECK_KEY = 'apk_update_last_check';
// APK 版本信息
export interface APKVersionInfo {
runtimeVersion: string;
versionName: string;
size: number;
downloadUrl: string;
createdAt: string;
}
// 版本比较结果
export interface VersionCheckResult {
hasUpdate: boolean;
currentVersion: string;
latestVersion: string;
downloadUrl: string;
size: number;
}
/**
* 比较版本号
* 返回: 1 表示 v1 > v2, -1 表示 v1 < v2, 0 表示相等
*/
function compareVersions(v1: string, v2: string): number {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const p1 = parts1[i] || 0;
const p2 = parts2[i] || 0;
if (p1 > p2) return 1;
if (p1 < p2) return -1;
}
return 0;
}
/**
* 获取最新 APK 版本信息
*/
async function fetchLatestAPKVersion(): Promise<APKVersionInfo | null> {
try {
const response = await fetch(`${UPDATES_SERVER_URL}/api/apk/latest`, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
console.warn('Failed to fetch latest APK version:', response.status);
return null;
}
const data: APKVersionInfo = await response.json();
return data;
} catch (error) {
console.error('Error fetching latest APK version:', error);
return null;
}
}
/**
* 获取当前应用版本
*/
function getCurrentVersion(): string {
return Constants.expoConfig?.version || '1.0.0';
}
/**
* 格式化文件大小
*/
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
/**
* 检查是否应该执行检查(避免频繁检查)
*/
async function shouldCheck(): Promise<boolean> {
try {
const lastCheck = await AsyncStorage.getItem(LAST_CHECK_KEY);
if (!lastCheck) return true;
const lastCheckTime = parseInt(lastCheck, 10);
return Date.now() - lastCheckTime > CHECK_INTERVAL;
} catch {
return true;
}
}
/**
* 记录检查时间
*/
async function recordCheckTime(): Promise<void> {
try {
await AsyncStorage.setItem(LAST_CHECK_KEY, Date.now().toString());
} catch (error) {
console.error('Failed to record check time:', error);
}
}
/**
* 下载并安装 APK
*/
async function downloadAndInstallAPK(downloadUrl: string, version: string): Promise<void> {
if (Platform.OS !== 'android') {
Alert.alert('提示', '此功能仅支持 Android 设备');
return;
}
if (!FileSystem) {
Alert.alert('提示', '当前环境不支持 APK 更新,请在浏览器中下载');
Linking.openURL(downloadUrl);
return;
}
try {
const downloadPath = `${FileSystem.documentDirectory}carrot-bbs-${version}.apk`;
// 显示下载进度
showConfirm({
title: '正在下载',
message: `正在下载新版本 ${version},请稍候...`,
confirmText: '后台下载',
cancelText: '取消',
onConfirm: () => {
// 用户选择后台下载,继续
},
onCancel: () => {
// 用户取消
},
});
// 下载 APK
const downloadResult = await FileSystem.downloadAsync(downloadUrl, downloadPath);
if (!downloadResult.uri) {
Alert.alert('下载失败', '无法下载安装包,请稍后重试');
return;
}
// 安装 APK
await installAPK(downloadResult.uri);
} catch (error) {
console.error('Download/Install error:', error);
Alert.alert('更新失败', '下载或安装过程中出错,请稍后重试');
}
}
/**
* 安装 APK
*/
async function installAPK(apkUri: string): Promise<void> {
try {
if (!FileSystem || !IntentLauncher) {
throw new Error('Native modules not available');
}
// 获取文件的 content URI
const contentUri = await FileSystem.getContentUriAsync(apkUri);
// 使用 Intent 安装 APK
await IntentLauncher.startActivityAsync('android.intent.action.INSTALL_PACKAGE', {
data: contentUri,
flags: 1, // FLAG_GRANT_READ_URI_PERMISSION
});
} catch (error) {
console.error('Install APK error:', error);
// 尝试使用浏览器下载
Linking.openURL(apkUri.replace('file://', ''));
}
}
/**
* 显示更新提示对话框
*/
function showUpdateDialog(result: VersionCheckResult): void {
const sizeText = formatFileSize(result.size);
showConfirm({
title: '发现新版本',
message: `当前版本: ${result.currentVersion}\n最新版本: ${result.latestVersion}\n安装包大小: ${sizeText}\n\n是否立即下载更新`,
confirmText: '立即更新',
cancelText: '稍后提醒',
onConfirm: () => {
downloadAndInstallAPK(result.downloadUrl, result.latestVersion);
},
});
}
/**
* 检查 APK 更新(主入口)
* @param force 是否强制检查(忽略时间间隔)
*/
export async function checkForAPKUpdate(force: boolean = false): Promise<VersionCheckResult | null> {
// 仅在 Android 平台检查
if (Platform.OS !== 'android') {
return null;
}
// 检查是否应该执行检查
if (!force) {
const should = await shouldCheck();
if (!should) {
console.log('APK update check skipped (too frequent)');
return null;
}
}
// 记录检查时间
await recordCheckTime();
// 获取最新版本信息
const latestAPK = await fetchLatestAPKVersion();
if (!latestAPK) {
console.log('No APK version info available');
return null;
}
const currentVersion = getCurrentVersion();
const latestVersion = latestAPK.versionName || latestAPK.runtimeVersion;
// 比较版本
const comparison = compareVersions(currentVersion, latestVersion);
const result: VersionCheckResult = {
hasUpdate: comparison < 0,
currentVersion,
latestVersion,
downloadUrl: latestAPK.downloadUrl,
size: latestAPK.size,
};
// 如果有更新,显示对话框
if (result.hasUpdate) {
showUpdateDialog(result);
}
return result;
}
/**
* 手动检查更新(用户主动触发)
*/
export async function manualCheckForUpdate(): Promise<void> {
const result = await checkForAPKUpdate(true);
if (!result) {
Alert.alert('检查失败', '无法获取版本信息,请检查网络连接');
return;
}
if (!result.hasUpdate) {
Alert.alert('已是最新', `当前版本 ${result.currentVersion} 已是最新版本`);
}
}
export const apkUpdateService = {
checkForAPKUpdate,
manualCheckForUpdate,
};