mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-04 03:36:29 +08:00
- Add toast error messages when version check fails - Show user-friendly error notifications for download failures - Display specific installation error messages with troubleshooting hints - Improve logging from console.error to console.info for better categorization
222 lines
7.7 KiB
TypeScript
222 lines
7.7 KiB
TypeScript
import ReactNativeBlobUtil from "react-native-blob-util";
|
||
import FileViewer from "react-native-file-viewer";
|
||
import Toast from "react-native-toast-message";
|
||
import { version as currentVersion } from "../package.json";
|
||
import { UPDATE_CONFIG } from "../constants/UpdateConfig";
|
||
|
||
interface VersionInfo {
|
||
version: string;
|
||
downloadUrl: string;
|
||
}
|
||
|
||
class UpdateService {
|
||
private static instance: UpdateService;
|
||
|
||
static getInstance(): UpdateService {
|
||
if (!UpdateService.instance) {
|
||
UpdateService.instance = new UpdateService();
|
||
}
|
||
return UpdateService.instance;
|
||
}
|
||
|
||
async checkVersion(): Promise<VersionInfo> {
|
||
let retries = 0;
|
||
const maxRetries = 3;
|
||
|
||
while (retries < maxRetries) {
|
||
try {
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
||
|
||
const response = await fetch(UPDATE_CONFIG.GITHUB_RAW_URL, {
|
||
signal: controller.signal,
|
||
});
|
||
|
||
clearTimeout(timeoutId);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: Failed to fetch version info`);
|
||
}
|
||
|
||
const remotePackage = await response.json();
|
||
const remoteVersion = remotePackage.version;
|
||
|
||
return {
|
||
version: remoteVersion,
|
||
downloadUrl: UPDATE_CONFIG.GITHUB_RELEASE_URL_TEMPLATE.replace(/{version}/g, remoteVersion),
|
||
};
|
||
} catch (error) {
|
||
retries++;
|
||
console.info(`Error checking version (attempt ${retries}/${maxRetries}):`, error);
|
||
|
||
if (retries === maxRetries) {
|
||
Toast.show({ type: "error", text1: "检查更新失败", text2: "无法获取版本信息,请检查网络连接" });
|
||
throw error;
|
||
}
|
||
|
||
// 等待一段时间后重试
|
||
await new Promise(resolve => setTimeout(resolve, 2000 * retries));
|
||
}
|
||
}
|
||
|
||
throw new Error("Maximum retry attempts exceeded");
|
||
}
|
||
|
||
// 清理旧的APK文件
|
||
private async cleanOldApkFiles(): Promise<void> {
|
||
try {
|
||
const { dirs } = ReactNativeBlobUtil.fs;
|
||
// 使用DocumentDir而不是DownloadDir
|
||
const files = await ReactNativeBlobUtil.fs.ls(dirs.DocumentDir);
|
||
|
||
// 查找所有OrionTV APK文件
|
||
const apkFiles = files.filter(file => file.startsWith('OrionTV_v') && file.endsWith('.apk'));
|
||
|
||
// 保留最新的2个文件,删除其他的
|
||
if (apkFiles.length > 2) {
|
||
const sortedFiles = apkFiles.sort((a, b) => {
|
||
// 从文件名中提取时间戳进行排序
|
||
const timeA = a.match(/OrionTV_v(\d+)\.apk/)?.[1] || '0';
|
||
const timeB = b.match(/OrionTV_v(\d+)\.apk/)?.[1] || '0';
|
||
return parseInt(timeB) - parseInt(timeA);
|
||
});
|
||
|
||
// 删除旧文件
|
||
const filesToDelete = sortedFiles.slice(2);
|
||
for (const file of filesToDelete) {
|
||
try {
|
||
await ReactNativeBlobUtil.fs.unlink(`${dirs.DocumentDir}/${file}`);
|
||
console.log(`Cleaned old APK file: ${file}`);
|
||
} catch (deleteError) {
|
||
console.warn(`Failed to delete old APK file ${file}:`, deleteError);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn('Failed to clean old APK files:', error);
|
||
}
|
||
}
|
||
|
||
async downloadApk(url: string, onProgress?: (progress: number) => void): Promise<string> {
|
||
let retries = 0;
|
||
const maxRetries = 3;
|
||
|
||
// 清理旧文件
|
||
await this.cleanOldApkFiles();
|
||
|
||
while (retries < maxRetries) {
|
||
try {
|
||
const { dirs } = ReactNativeBlobUtil.fs;
|
||
const timestamp = new Date().getTime();
|
||
const fileName = `OrionTV_v${timestamp}.apk`;
|
||
// 使用应用的外部文件目录,而不是系统下载目录
|
||
const filePath = `${dirs.DocumentDir}/${fileName}`;
|
||
|
||
const task = ReactNativeBlobUtil.config({
|
||
fileCache: true,
|
||
path: filePath,
|
||
timeout: UPDATE_CONFIG.DOWNLOAD_TIMEOUT,
|
||
// 移除 addAndroidDownloads 配置,避免使用系统下载管理器
|
||
// addAndroidDownloads: {
|
||
// useDownloadManager: true,
|
||
// notification: true,
|
||
// title: UPDATE_CONFIG.NOTIFICATION.TITLE,
|
||
// description: UPDATE_CONFIG.NOTIFICATION.DOWNLOADING_TEXT,
|
||
// mime: "application/vnd.android.package-archive",
|
||
// mediaScannable: true,
|
||
// },
|
||
}).fetch("GET", url);
|
||
|
||
// 监听下载进度
|
||
if (onProgress) {
|
||
task.progress((received: string, total: string) => {
|
||
const receivedNum = parseInt(received, 10);
|
||
const totalNum = parseInt(total, 10);
|
||
const progress = Math.floor((receivedNum / totalNum) * 100);
|
||
onProgress(progress);
|
||
});
|
||
}
|
||
|
||
const res = await task;
|
||
console.log(`APK downloaded successfully: ${filePath}`);
|
||
return res.path();
|
||
} catch (error) {
|
||
retries++;
|
||
console.info(`Error downloading APK (attempt ${retries}/${maxRetries}):`, error);
|
||
|
||
if (retries === maxRetries) {
|
||
Toast.show({ type: "error", text1: "下载失败", text2: "APK下载失败,请检查网络连接" });
|
||
throw new Error(`Download failed after ${maxRetries} attempts: ${error}`);
|
||
}
|
||
|
||
// 等待一段时间后重试
|
||
await new Promise(resolve => setTimeout(resolve, 3000 * retries));
|
||
}
|
||
}
|
||
|
||
throw new Error("Maximum retry attempts exceeded for download");
|
||
}
|
||
|
||
async installApk(filePath: string): Promise<void> {
|
||
try {
|
||
// 首先检查文件是否存在
|
||
const exists = await ReactNativeBlobUtil.fs.exists(filePath);
|
||
if (!exists) {
|
||
throw new Error(`APK file not found: ${filePath}`);
|
||
}
|
||
|
||
// 使用FileViewer打开APK文件进行安装
|
||
// 这会触发Android的包安装器
|
||
await FileViewer.open(filePath, {
|
||
showOpenWithDialog: true, // 显示选择应用对话框
|
||
showAppsSuggestions: true, // 显示应用建议
|
||
displayName: "OrionTV Update",
|
||
});
|
||
} catch (error) {
|
||
console.info("Error installing APK:", error);
|
||
|
||
// 提供更详细的错误信息
|
||
if (error instanceof Error) {
|
||
if (error.message.includes('No app found')) {
|
||
Toast.show({ type: "error", text1: "安装失败", text2: "未找到可安装APK的应用,请确保允许安装未知来源的应用" });
|
||
throw new Error('未找到可安装APK的应用,请确保允许安装未知来源的应用');
|
||
} else if (error.message.includes('permission')) {
|
||
Toast.show({ type: "error", text1: "安装失败", text2: "没有安装权限,请在设置中允许此应用安装未知来源的应用" });
|
||
throw new Error('没有安装权限,请在设置中允许此应用安装未知来源的应用');
|
||
} else {
|
||
Toast.show({ type: "error", text1: "安装失败", text2: "APK安装过程中出现错误" });
|
||
}
|
||
} else {
|
||
Toast.show({ type: "error", text1: "安装失败", text2: "APK安装过程中出现未知错误" });
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
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 part1 = parts1[i] || 0;
|
||
const part2 = parts2[i] || 0;
|
||
|
||
if (part1 > part2) return 1;
|
||
if (part1 < part2) return -1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
getCurrentVersion(): string {
|
||
return currentVersion;
|
||
}
|
||
|
||
isUpdateAvailable(remoteVersion: string): boolean {
|
||
return this.compareVersions(remoteVersion, currentVersion) > 0;
|
||
}
|
||
}
|
||
|
||
export default UpdateService.getInstance();
|