2026-03-26 03:41:43 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* APK 更新检查服务
|
|
|
|
|
|
* 用于检查最新版本并提示用户下载更新
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { Platform, Linking, Alert } from 'react-native';
|
|
|
|
|
|
import Constants from 'expo-constants';
|
|
|
|
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
2026-04-13 01:30:37 +08:00
|
|
|
|
import { showConfirm } from '../ui/dialogService';
|
2026-03-26 03:41:43 +08:00
|
|
|
|
|
2026-03-26 03:45:39 +08:00
|
|
|
|
// 条件导入原生模块(仅在 Android 上可用)
|
2026-03-26 16:46:22 +08:00
|
|
|
|
// 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;
|
2026-03-26 03:45:39 +08:00
|
|
|
|
|
2026-03-26 03:41:43 +08:00
|
|
|
|
// 更新服务器地址
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 03:45:39 +08:00
|
|
|
|
if (!FileSystem) {
|
|
|
|
|
|
Alert.alert('提示', '当前环境不支持 APK 更新,请在浏览器中下载');
|
|
|
|
|
|
Linking.openURL(downloadUrl);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 03:41:43 +08:00
|
|
|
|
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 {
|
2026-03-26 03:45:39 +08:00
|
|
|
|
if (!FileSystem || !IntentLauncher) {
|
|
|
|
|
|
throw new Error('Native modules not available');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 03:41:43 +08:00
|
|
|
|
// 获取文件的 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);
|
|
|
|
|
|
// 尝试使用浏览器下载
|
2026-03-26 03:45:39 +08:00
|
|
|
|
Linking.openURL(apkUri.replace('file://', ''));
|
2026-03-26 03:41:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 显示更新提示对话框
|
|
|
|
|
|
*/
|
|
|
|
|
|
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,
|
|
|
|
|
|
};
|