fix(update): resolve APK download path issue and enhance update components

- Fix UpdateService to use DocumentDir instead of DownloadDir for APK storage
- Add retry mechanism for network failures in version checking and downloading
- Implement automatic cleanup of old APK files to manage storage
- Replace TouchableOpacity with StyledButton in UpdateModal for consistency
- Add TV focus control to UpdateSection component
- Reduce category button spacing on TV for better navigation
- Update download URL template to match release naming convention
This commit is contained in:
zimplexing
2025-08-13 17:19:48 +08:00
parent 9e9e4597cc
commit f0c797434d
7 changed files with 241 additions and 228 deletions

View File

@@ -1,6 +1,7 @@
import ReactNativeBlobUtil from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
import { version as currentVersion } from "../package.json";
import { UPDATE_CONFIG } from "../constants/UpdateConfig";
interface VersionInfo {
version: string;
@@ -18,75 +19,168 @@ class UpdateService {
}
async checkVersion(): Promise<VersionInfo> {
try {
const response = await fetch(
"https://raw.githubusercontent.com/zimplexing/OrionTV/refs/heads/master/package.json"
);
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("Failed to fetch version info");
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.error(`Error checking version (attempt ${retries}/${maxRetries}):`, error);
if (retries === maxRetries) {
throw error;
}
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 2000 * retries));
}
}
throw new Error("Maximum retry attempts exceeded");
}
const remotePackage = await response.json();
const remoteVersion = remotePackage.version;
return {
version: remoteVersion,
downloadUrl: `https://github.com/zimplexing/OrionTV/releases/download/v${remoteVersion}/orionTV.
${remoteVersion}.apk`,
};
// 清理旧的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.error("Error checking version:", error);
throw error;
console.warn('Failed to clean old APK files:', error);
}
}
async downloadApk(url: string, onProgress?: (progress: number) => void): Promise<string> {
try {
const { dirs } = ReactNativeBlobUtil.fs;
const fileName = `OrionTV_v${new Date().getTime()}.apk`;
const filePath = `${dirs.DownloadDir}/${fileName}`;
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,
addAndroidDownloads: {
useDownloadManager: true,
notification: true,
title: "OrionTV 更新下载中",
description: "正在下载新版本...",
mime: "application/vnd.android.package-archive",
mediaScannable: true,
},
}).fetch("GET", url);
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);
});
// 监听下载进度
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.error(`Error downloading APK (attempt ${retries}/${maxRetries}):`, error);
if (retries === maxRetries) {
throw new Error(`Download failed after ${maxRetries} attempts: ${error}`);
}
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 3000 * retries));
}
const res = await task;
return res.path();
} catch (error) {
console.error("Error downloading APK:", error);
throw error;
}
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: false,
showAppsSuggestions: false,
showOpenWithDialog: true, // 显示选择应用对话框
showAppsSuggestions: true, // 显示应用建议
displayName: "OrionTV Update",
});
} catch (error) {
console.error("Error installing APK:", error);
// 提供更详细的错误信息
if (error instanceof Error) {
if (error.message.includes('No app found')) {
throw new Error('未找到可安装APK的应用请确保允许安装未知来源的应用');
} else if (error.message.includes('permission')) {
throw new Error('没有安装权限,请在设置中允许此应用安装未知来源的应用');
}
}
throw error;
}
}