/** * 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 上可用) let FileSystem: typeof import('expo-file-system/legacy') | null = null; let IntentLauncher: typeof import('expo-intent-launcher') | null = null; if (Platform.OS === 'android') { try { FileSystem = require('expo-file-system/legacy'); IntentLauncher = require('expo-intent-launcher'); } catch (e) { console.warn('Failed to load native modules for APK update:', e); } } // 更新服务器地址 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 { 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 { 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 { 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 { 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 { 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 { // 仅在 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 { const result = await checkForAPKUpdate(true); if (!result) { Alert.alert('检查失败', '无法获取版本信息,请检查网络连接'); return; } if (!result.hasUpdate) { Alert.alert('已是最新', `当前版本 ${result.currentVersion} 已是最新版本`); } } export const apkUpdateService = { checkForAPKUpdate, manualCheckForUpdate, };