|
|
@@ -1,1233 +0,0 @@
|
|
|
-import { hilog } from '@kit.PerformanceAnalysisKit';
|
|
|
-import { formProvider, formBindingData } from '@kit.FormKit';
|
|
|
-import { preferences } from '@kit.ArkData';
|
|
|
-import { Context } from '@kit.AbilityKit';
|
|
|
-import { WidgetData, FormattedWidgetData, WidgetSize, ImageFileInfo } from './WidgetTypes';
|
|
|
-import { FormLayoutManager } from './FormLayoutManager';
|
|
|
-import { PreferencesUtil } from '../utils/PreferencesUtil';
|
|
|
-import { GlobalWidgetManager } from './GlobalWidgetManager';
|
|
|
-import { fileIo } from '@kit.CoreFileKit';
|
|
|
-import { http } from '@kit.NetworkKit';
|
|
|
-
|
|
|
-const TAG = 'EnhancedFormUpdateService';
|
|
|
-
|
|
|
-/**
|
|
|
- * 表单状态数据接口
|
|
|
- */
|
|
|
-interface FormStateData extends Record<string, string | number | boolean | Uint8Array> {
|
|
|
- size: string;
|
|
|
- lastUpdate: number;
|
|
|
- lastSongId: string;
|
|
|
- updateCount: number;
|
|
|
- lastImageUrl: string;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 更新结果接口
|
|
|
- */
|
|
|
-interface UpdateResult {
|
|
|
- formId: string;
|
|
|
- success: boolean;
|
|
|
- error?: Error;
|
|
|
- updateTime: number;
|
|
|
- retryCount: number;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 批量更新统计
|
|
|
- */
|
|
|
-interface BatchUpdateStats {
|
|
|
- total: number;
|
|
|
- success: number;
|
|
|
- failed: number;
|
|
|
- duration: number;
|
|
|
- averageUpdateTime: number;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 网络图片缓存项
|
|
|
- */
|
|
|
-interface ImageCacheItem {
|
|
|
- fileName: string;
|
|
|
- downloadTime: number;
|
|
|
- expiry: number;
|
|
|
- fileSize: number;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 图片下载结果接口
|
|
|
- */
|
|
|
-interface ImageDownloadResult {
|
|
|
- fileName: string;
|
|
|
- fd?: number;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 包含图片文件描述符的卡片数据接口
|
|
|
- */
|
|
|
-interface FormattedWidgetDataWithImages extends FormattedWidgetData {
|
|
|
- formImages?: Record<string, number>;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 处理后的本地图片信息接口
|
|
|
- */
|
|
|
-interface ProcessedImageInfo {
|
|
|
- fileName: string;
|
|
|
- memoryUri: string;
|
|
|
- fd: number;
|
|
|
- sourceHash?: string; // 添加源文件哈希用于缓存标识
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 更新统计接口
|
|
|
- */
|
|
|
-export interface UpdateStats {
|
|
|
- totalUpdates: number;
|
|
|
- successfulUpdates: number;
|
|
|
- failedUpdates: number;
|
|
|
- averageUpdateTime: number;
|
|
|
- lastBatchStats?: BatchUpdateStats;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 增强版卡片更新服务
|
|
|
- * 在原有DirectFormUpdateService基础上添加了以下功能:
|
|
|
- * 1. 智能重试机制
|
|
|
- * 2. 网络图片缓存
|
|
|
- * 3. 批量更新统计
|
|
|
- * 4. 性能监控
|
|
|
- * 5. 错误恢复
|
|
|
- */
|
|
|
-export class EnhancedFormUpdateService {
|
|
|
- private static instance: EnhancedFormUpdateService | null = null;
|
|
|
- private layoutManager: FormLayoutManager = FormLayoutManager.getInstance();
|
|
|
- private preferencesUtil: PreferencesUtil = PreferencesUtil.getInstance();
|
|
|
- private globalWidgetManager: GlobalWidgetManager = GlobalWidgetManager.getInstance();
|
|
|
- private appContext: Context | null = null;
|
|
|
-
|
|
|
- // 防抖和节流
|
|
|
- private lastUpdateTime: number = 0;
|
|
|
- private updateDebounceDelay: number = 100; // 100ms防抖延迟
|
|
|
- private isUpdating: boolean = false;
|
|
|
-
|
|
|
- // 重试配置
|
|
|
- private readonly maxRetryCount: number = 3;
|
|
|
- private readonly retryDelays: number[] = [500, 1000, 2000]; // 递增重试延迟
|
|
|
-
|
|
|
- // 网络图片缓存
|
|
|
- private imageCache: Map<string, ImageCacheItem> = new Map();
|
|
|
- private downloadingImages: Map<string, Promise<ImageDownloadResult | null>> = new Map();
|
|
|
- private readonly imageCacheExpiry: number = 24 * 60 * 60 * 1000; // 24小时
|
|
|
- private readonly maxCacheSize: number = 50; // 最多缓存50张图片
|
|
|
-
|
|
|
- // 本地图片处理缓存
|
|
|
- private localImageCache: Map<string, ProcessedImageInfo> = new Map();
|
|
|
- private processingLocalImages: Map<string, Promise<ProcessedImageInfo | null>> = new Map();
|
|
|
- private readonly localImageCacheExpiry: number = 30 * 60 * 1000; // 30分钟
|
|
|
- private lastLocalCacheCleanup: number = 0;
|
|
|
-
|
|
|
- // 性能统计
|
|
|
- private updateStats: UpdateStats = {
|
|
|
- totalUpdates: 0,
|
|
|
- successfulUpdates: 0,
|
|
|
- failedUpdates: 0,
|
|
|
- averageUpdateTime: 0
|
|
|
- };
|
|
|
-
|
|
|
- private constructor() {}
|
|
|
-
|
|
|
- public static getInstance(): EnhancedFormUpdateService {
|
|
|
- if (!EnhancedFormUpdateService.instance) {
|
|
|
- EnhancedFormUpdateService.instance = new EnhancedFormUpdateService();
|
|
|
- }
|
|
|
- return EnhancedFormUpdateService.instance;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 设置应用上下文
|
|
|
- */
|
|
|
- public setAppContext(context: Context): void {
|
|
|
- this.appContext = context;
|
|
|
-
|
|
|
- this.initializeImageCache();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 主要的更新所有卡片方法 - 参考原DirectFormUpdateService.updateAllForms()
|
|
|
- */
|
|
|
- public async updateAllForms(data: WidgetData): Promise<BatchUpdateStats> {
|
|
|
- const batchStartTime = Date.now();
|
|
|
- console.log("Heanup updateAllForms data:"+JSON.stringify(data))
|
|
|
-
|
|
|
- // 预处理图片路径 - 转换本地文件URI为可用格式
|
|
|
- const processedData = await this.preprocessImageData(data);
|
|
|
-
|
|
|
- try {
|
|
|
- if (!this.appContext) {
|
|
|
- hilog.error(0x0000, TAG, '❌ App context not set, cannot update forms');
|
|
|
- throw new Error('App context not set');
|
|
|
- }
|
|
|
-
|
|
|
- // 防抖检查
|
|
|
- const now = Date.now();
|
|
|
- if (now - this.lastUpdateTime < this.updateDebounceDelay) {
|
|
|
-
|
|
|
- return this.createEmptyStats();
|
|
|
- }
|
|
|
-
|
|
|
- // 防止并发更新
|
|
|
- if (this.isUpdating) {
|
|
|
-
|
|
|
- return this.createEmptyStats();
|
|
|
- }
|
|
|
-
|
|
|
- this.isUpdating = true;
|
|
|
- this.lastUpdateTime = now;
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- // 获取所有持久化的 Form ID(使用原有方法)
|
|
|
- const prefs = await this.preferencesUtil.getPreferences(this.appContext);
|
|
|
- const formIds = await this.preferencesUtil.getFormIds(prefs);
|
|
|
-
|
|
|
- if (formIds.length === 0) {
|
|
|
-
|
|
|
- return this.createEmptyStats();
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- // 验证活跃卡片与持久化卡片的一致性
|
|
|
- await this.validateActiveWidgets(formIds, prefs);
|
|
|
-
|
|
|
- // 预先验证卡片ID的有效性,移除无效的ID
|
|
|
- const validFormIds = await this.preValidateFormIds(formIds, prefs);
|
|
|
-
|
|
|
- if (validFormIds.length === 0) {
|
|
|
- hilog.warn(0x0000, TAG, '📋 No valid form IDs found after validation');
|
|
|
- return this.createEmptyStats();
|
|
|
- }
|
|
|
-
|
|
|
- hilog.info(0x0000, TAG, `📋 Valid forms: ${validFormIds.length}/${formIds.length}`);
|
|
|
-
|
|
|
- // 并行更新所有卡片
|
|
|
- const updatePromises = validFormIds.map((formId, index) => {
|
|
|
-
|
|
|
-
|
|
|
- return this.updateSingleFormWithRetry(formId, processedData, prefs);
|
|
|
- });
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- // 等待所有更新完成
|
|
|
- const results = await Promise.allSettled(updatePromises);
|
|
|
-
|
|
|
- // 处理结果并生成统计信息
|
|
|
- const batchStats = this.processBatchResults(validFormIds, results, batchStartTime);
|
|
|
-
|
|
|
- // 清理无效的 Form ID
|
|
|
- if (batchStats.failed > 0) {
|
|
|
- await this.cleanupInvalidForms(prefs, validFormIds, results);
|
|
|
- }
|
|
|
-
|
|
|
- return batchStats;
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- hilog.error(0x0000, TAG, `❌ Failed to update forms: ${error}`);
|
|
|
- throw new Error(`Failed to update forms: ${error}`);
|
|
|
- } finally {
|
|
|
- this.isUpdating = false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 带重试机制的单个卡片更新
|
|
|
- */
|
|
|
- private async updateSingleFormWithRetry(formId: string, data: WidgetData, prefs: preferences.Preferences): Promise<UpdateResult> {
|
|
|
- const startTime = Date.now();
|
|
|
- let lastError: Error | null = null;
|
|
|
-
|
|
|
- for (let attempt = 0; attempt <= this.maxRetryCount; attempt++) {
|
|
|
- try {
|
|
|
- // if (attempt > 0) {
|
|
|
- // const delay = this.retryDelays[Math.min(attempt - 1, this.retryDelays.length - 1)];
|
|
|
- //
|
|
|
- // await this.sleep(delay);
|
|
|
- // }
|
|
|
-
|
|
|
- await this.updateSingleForm(formId, data, prefs);
|
|
|
-
|
|
|
- const updateTime = Date.now() - startTime;
|
|
|
-
|
|
|
-
|
|
|
- return {
|
|
|
- formId,
|
|
|
- success: true,
|
|
|
- updateTime,
|
|
|
- retryCount: attempt
|
|
|
- };
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- lastError = error as Error;
|
|
|
- const errorStr :string= error.toString();
|
|
|
-
|
|
|
- // 如果是无效卡片ID错误,立即停止重试并清理
|
|
|
- if (errorStr.includes('form not exist') ||
|
|
|
- errorStr.includes('16501001') ||
|
|
|
- errorStr.includes('The ID of the form to be operated does not exist')) {
|
|
|
- hilog.warn(0x0000, TAG, `🗑️ [${formId}] Invalid form ID detected, cleaning up immediately`);
|
|
|
-
|
|
|
- // 立即清理无效ID
|
|
|
- try {
|
|
|
- await this.preferencesUtil.removeFormId(prefs, formId);
|
|
|
- this.globalWidgetManager.unregisterWidget(formId);
|
|
|
- hilog.info(0x0000, TAG, `🗑️ [${formId}] Cleaned up invalid form ID`);
|
|
|
- } catch (cleanupError) {
|
|
|
- hilog.error(0x0000, TAG, `❌ [${formId}] Failed to cleanup invalid form ID: ${cleanupError}`);
|
|
|
- }
|
|
|
-
|
|
|
- // 立即返回失败结果,不再重试
|
|
|
- const updateTime = Date.now() - startTime;
|
|
|
- return {
|
|
|
- formId,
|
|
|
- success: false,
|
|
|
- error: lastError,
|
|
|
- updateTime,
|
|
|
- retryCount: attempt
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const updateTime = Date.now() - startTime;
|
|
|
- hilog.error(0x0000, TAG, `❌ [${formId}] All ${this.maxRetryCount + 1} attempts failed (${updateTime}ms total)`);
|
|
|
-
|
|
|
- return {
|
|
|
- formId,
|
|
|
- success: false,
|
|
|
- error: lastError || new Error('Unknown error'),
|
|
|
- updateTime,
|
|
|
- retryCount: this.maxRetryCount
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 更新单个卡片(增强版,参考原有方法)
|
|
|
- */
|
|
|
- private async updateSingleForm(formId: string, data: WidgetData, prefs: preferences.Preferences): Promise<void> {
|
|
|
- try {
|
|
|
-
|
|
|
-
|
|
|
- // 获取卡片的当前状态
|
|
|
- const formState = await this.preferencesUtil.getFormState(prefs, formId);
|
|
|
- const widgetSizeStr = (formState?.size as string) || 'medium';
|
|
|
- const widgetSize: WidgetSize = widgetSizeStr as WidgetSize;
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- // 适配数据到卡片尺寸
|
|
|
- const adaptedData = this.layoutManager.adaptDataForSize(data, widgetSize);
|
|
|
-
|
|
|
- // 处理网络图片(传递原始数据以便设置文件描述符)
|
|
|
- await this.handleNetworkImage(adaptedData, data);
|
|
|
-
|
|
|
- // 转换为 FormattedWidgetData
|
|
|
- const formattedData: FormattedWidgetData = {
|
|
|
- isPlaying: adaptedData.isPlaying,
|
|
|
- isPaused: adaptedData.isPaused,
|
|
|
- isLoading: adaptedData.isLoading,
|
|
|
- songTitle: adaptedData.songTitle,
|
|
|
- songArtist: adaptedData.songArtist,
|
|
|
- songAlbum: adaptedData.songAlbum,
|
|
|
- coverImage: adaptedData.coverImage,
|
|
|
- currentTime: adaptedData.currentTime,
|
|
|
- totalTime: adaptedData.totalTime,
|
|
|
- progressPercentage: adaptedData.progressPercentage,
|
|
|
- hasNext: adaptedData.hasNext,
|
|
|
- hasPrevious: adaptedData.hasPrevious,
|
|
|
- showProgress: adaptedData.showProgress,
|
|
|
- showCover: adaptedData.showCover,
|
|
|
- widgetSize: adaptedData.widgetSize,
|
|
|
- timestamp: Date.now(),
|
|
|
- imgName: adaptedData.imgName || '',
|
|
|
- isFavorite: adaptedData.isFavorite,
|
|
|
- };
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- // 处理图片文件描述符(如果有本地图片)
|
|
|
- let formData: formBindingData.FormBindingData;
|
|
|
- if (data.imageFileInfo && formattedData.imgName) {
|
|
|
- // 创建包含文件描述符的数据
|
|
|
- const dataWithImages: FormattedWidgetDataWithImages = {
|
|
|
- isPlaying: formattedData.isPlaying,
|
|
|
- isPaused: formattedData.isPaused,
|
|
|
- isLoading: formattedData.isLoading,
|
|
|
- songTitle: formattedData.songTitle,
|
|
|
- songArtist: formattedData.songArtist,
|
|
|
- songAlbum: formattedData.songAlbum,
|
|
|
- coverImage: formattedData.coverImage,
|
|
|
- currentTime: formattedData.currentTime,
|
|
|
- totalTime: formattedData.totalTime,
|
|
|
- progressPercentage: formattedData.progressPercentage,
|
|
|
- hasNext: formattedData.hasNext,
|
|
|
- hasPrevious: formattedData.hasPrevious,
|
|
|
- showProgress: formattedData.showProgress,
|
|
|
- showCover: formattedData.showCover,
|
|
|
- widgetSize: formattedData.widgetSize,
|
|
|
- timestamp: formattedData.timestamp,
|
|
|
- imgName: formattedData.imgName,
|
|
|
- formImages: {} as Record<string, number>,
|
|
|
- isFavorite: formattedData.isFavorite
|
|
|
- };
|
|
|
-
|
|
|
- // 设置图片文件描述符
|
|
|
- // 根据官方文档:imgName 必须和 formImages 中的 key 相同
|
|
|
- if (dataWithImages.formImages && data.imageFileInfo.fileName) {
|
|
|
- dataWithImages.formImages[data.imageFileInfo.fileName] = data.imageFileInfo.fd;
|
|
|
- // 确保 imgName 与 formImages 的 key 一致
|
|
|
- dataWithImages.imgName = data.imageFileInfo.fileName;
|
|
|
- }
|
|
|
-
|
|
|
- formData = formBindingData.createFormBindingData(dataWithImages);
|
|
|
- hilog.info(0x0000, TAG, `📷 [${formId}] Created form data with image fd: ${data.imageFileInfo.fd} for ${data.imageFileInfo.fileName}`);
|
|
|
- } else {
|
|
|
- formData = formBindingData.createFormBindingData(formattedData);
|
|
|
- }
|
|
|
-
|
|
|
- // 更新卡片
|
|
|
- await formProvider.updateForm(formId, formData);
|
|
|
-
|
|
|
- // 注意:不手动关闭文件描述符,让系统自动管理
|
|
|
- // 手动关闭可能导致EBADF错误,因为卡片系统可能还在使用文件描述符
|
|
|
- if (data.imageFileInfo && data.imageFileInfo.fd !== undefined && data.imageFileInfo.fd > 0) {
|
|
|
- hilog.info(0x0000, TAG, `📷 [${formId}] Form updated with file descriptor: ${data.imageFileInfo.fd} for ${data.imageFileInfo.fileName}`);
|
|
|
- // 将fd标记为已使用,但不关闭,让系统自动回收
|
|
|
- data.imageFileInfo.fd = -1;
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- // 保存增强状态信息
|
|
|
- await this.saveEnhancedFormState(prefs, formId, widgetSizeStr, data);
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- hilog.error(0x0000, TAG, `❌ [${formId}] Failed to update form: ${error}`);
|
|
|
- throw new Error(`Failed to update form ${formId}: ${error}`);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 预处理图片数据 - 转换本地文件URI为卡片可用格式
|
|
|
- */
|
|
|
- private async preprocessImageData(data: WidgetData): Promise<WidgetData> {
|
|
|
- try {
|
|
|
- // 修改点1: 显式声明processedData的类型为WidgetData
|
|
|
- let processedData: WidgetData = data;
|
|
|
- const coverImagePath = data.currentSong.coverImagePath;
|
|
|
-
|
|
|
- if (!coverImagePath || coverImagePath.trim() === '') {
|
|
|
- hilog.info(0x0000, TAG, '📷 No cover image path, skipping preprocessing');
|
|
|
- return data;
|
|
|
- }
|
|
|
-
|
|
|
- hilog.info(0x0000, TAG, `📷 Processing cover image: ${coverImagePath}`);
|
|
|
-
|
|
|
- // 处理本地文件URI
|
|
|
- if (this.isLocalFileUri(coverImagePath)) {
|
|
|
- hilog.info(0x0000, TAG, `📷 Detected local file URI: ${coverImagePath}`);
|
|
|
-
|
|
|
- const processedImageInfo = await this.processLocalImageFile(coverImagePath);
|
|
|
-
|
|
|
- if (processedImageInfo) {
|
|
|
- // 创建新的数据对象,包含处理后的图片信息
|
|
|
- processedData.currentSong.coverImagePath = processedImageInfo.memoryUri;
|
|
|
- processedData.imageFileInfo = processedImageInfo as ImageFileInfo;
|
|
|
-
|
|
|
- hilog.info(0x0000, TAG, `📷 Local image processed successfully: ${processedImageInfo.fileName} -> ${processedImageInfo.memoryUri}`);
|
|
|
- return processedData;
|
|
|
- } else {
|
|
|
- hilog.warn(0x0000, TAG, `📷 Failed to process local image, using original data`);
|
|
|
- return data;
|
|
|
- }
|
|
|
- }
|
|
|
- // 处理网络图片
|
|
|
- else if (this.isNetworkUrl(coverImagePath)) {
|
|
|
- hilog.info(0x0000, TAG, `📷 Detected network image: ${coverImagePath}`);
|
|
|
- // 网络图片在 handleNetworkImage 中处理
|
|
|
- return data;
|
|
|
- }
|
|
|
- // 其他情况
|
|
|
- else {
|
|
|
- hilog.info(0x0000, TAG, `📷 Using image path as-is: ${coverImagePath}`);
|
|
|
- return data;
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- hilog.error(0x0000, TAG, `❌ Error preprocessing image data: ${error}`);
|
|
|
- return data; // 出错时返回原始数据
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理本地图片文件
|
|
|
- */
|
|
|
- private async processLocalImageFile(fileUri: string): Promise<ProcessedImageInfo | null> {
|
|
|
- try {
|
|
|
- hilog.info(0x0000, TAG, `📷 Processing local image file: ${fileUri}`);
|
|
|
-
|
|
|
- // 生成缓存键(基于URI和时间戳)
|
|
|
- const cacheKey = this.generateLocalImageCacheKey(fileUri);
|
|
|
-
|
|
|
- // 检查本地图片缓存
|
|
|
- const cachedInfo = this.localImageCache.get(cacheKey);
|
|
|
- if (cachedInfo) {
|
|
|
- // 验证缓存的文件是否还存在
|
|
|
- const tempFilePath = `${this.appContext!.getApplicationContext().tempDir}/${cachedInfo.fileName}`;
|
|
|
- if (fileIo.accessSync(tempFilePath)) {
|
|
|
- hilog.info(0x0000, TAG, `📷 Using cached local image: ${cachedInfo.fileName}`);
|
|
|
- return cachedInfo;
|
|
|
- } else {
|
|
|
- // 缓存文件不存在,移除缓存项
|
|
|
- this.localImageCache.delete(cacheKey);
|
|
|
- hilog.warn(0x0000, TAG, `📷 Cached file not found, will reprocess: ${cachedInfo.fileName}`);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 检查是否正在处理中
|
|
|
- if (this.processingLocalImages.has(cacheKey)) {
|
|
|
- hilog.info(0x0000, TAG, `📷 Image already being processed, waiting: ${fileUri}`);
|
|
|
- return await this.processingLocalImages.get(cacheKey)!;
|
|
|
- }
|
|
|
-
|
|
|
- // 创建处理Promise
|
|
|
- const processingPromise = this.performLocalImageProcessing(fileUri, cacheKey);
|
|
|
- this.processingLocalImages.set(cacheKey, processingPromise);
|
|
|
-
|
|
|
- try {
|
|
|
- const result = await processingPromise;
|
|
|
- return result;
|
|
|
- } finally {
|
|
|
- this.processingLocalImages.delete(cacheKey);
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- hilog.error(0x0000, TAG, `❌ Failed to process local image: ${error}`);
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成本地图片缓存键
|
|
|
- */
|
|
|
- private generateLocalImageCacheKey(fileUri: string): string {
|
|
|
- // 从URI中提取文件名或路径的关键部分作为缓存键
|
|
|
- const pathParts = fileUri.split('/');
|
|
|
- const fileName = pathParts[pathParts.length - 1];
|
|
|
- // 使用文件名作为缓存键(因为日志显示相同的文件被重复处理)
|
|
|
- return `local_${fileName}`;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 执行本地图片处理
|
|
|
- */
|
|
|
- private async performLocalImageProcessing(fileUri: string, cacheKey: string): Promise<ProcessedImageInfo | null> {
|
|
|
- try {
|
|
|
- // 清理过期缓存
|
|
|
- this.cleanupLocalImageCache();
|
|
|
-
|
|
|
- // 转换 file:// URI 为实际文件路径
|
|
|
- // file://com.xgplayer.ttmusic.hm/data/storage/el2/base/haps/entry/files/xxx.jpg
|
|
|
- // 转换为 /data/storage/el2/base/haps/entry/files/xxx.jpg
|
|
|
- let realPath = fileUri.replace('file://', '');
|
|
|
-
|
|
|
- // 如果路径包含应用包名,需要移除它并构建正确的绝对路径
|
|
|
- if (realPath.startsWith('com.xgplayer.ttmusic.hm/')) {
|
|
|
- // 移除包名前缀,获取相对路径
|
|
|
- const relativePath = realPath.replace('com.xgplayer.ttmusic.hm/', '');
|
|
|
- // 使用应用上下文获取正确的文件路径
|
|
|
- realPath = `${this.appContext!.filesDir}/${relativePath.split('/').pop()}`;
|
|
|
- hilog.info(0x0000, TAG, `📷 Converted path with package name: ${fileUri} -> ${realPath}`);
|
|
|
- } else if (!realPath.startsWith('/')) {
|
|
|
- // 如果不是绝对路径,添加根路径
|
|
|
- realPath = `/${realPath}`;
|
|
|
- }
|
|
|
-
|
|
|
- hilog.info(0x0000, TAG, `📷 Final resolved path: ${realPath}`);
|
|
|
-
|
|
|
- // 检查源文件是否存在
|
|
|
- if (!fileIo.accessSync(realPath)) {
|
|
|
- hilog.error(0x0000, TAG, `📷 Source image file does not exist: ${realPath}`);
|
|
|
-
|
|
|
- // 尝试备用路径查找
|
|
|
- const fileName = realPath.split('/').pop();
|
|
|
- const alternativePaths = [
|
|
|
- `${this.appContext!.filesDir}/${fileName}`,
|
|
|
- `${this.appContext!.cacheDir}/${fileName}`,
|
|
|
- `${this.appContext!.tempDir}/${fileName}`
|
|
|
- ];
|
|
|
-
|
|
|
- let foundPath: string | null = null;
|
|
|
- for (const altPath of alternativePaths) {
|
|
|
- if (fileIo.accessSync(altPath)) {
|
|
|
- foundPath = altPath;
|
|
|
- hilog.info(0x0000, TAG, `📷 Found file at alternative path: ${altPath}`);
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (!foundPath) {
|
|
|
- hilog.error(0x0000, TAG, `📷 File not found in any location: ${fileName}`);
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- realPath = foundPath;
|
|
|
- }
|
|
|
-
|
|
|
- // 生成目标文件名(确保每次都不同,符合官方文档要求)
|
|
|
- const fileExtension = this.getFileExtension(realPath) || 'jpg';
|
|
|
- const fileName = `widget_local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.${fileExtension}`;
|
|
|
-
|
|
|
- // 使用 FormExtensionAbility 的 tempDir(官方文档要求)
|
|
|
- const formTempDir = this.appContext!.getApplicationContext().tempDir;
|
|
|
- const tempFilePath = `${formTempDir}/${fileName}`;
|
|
|
-
|
|
|
- hilog.info(0x0000, TAG, `📷 Using tempDir: ${formTempDir}`);
|
|
|
-
|
|
|
- // 复制文件到临时目录
|
|
|
- fileIo.copyFileSync(realPath, tempFilePath);
|
|
|
-
|
|
|
- // 获取文件描述符
|
|
|
- const file = fileIo.openSync(tempFilePath, fileIo.OpenMode.READ_ONLY);
|
|
|
- const fd = file.fd;
|
|
|
-
|
|
|
- const memoryUri = `memory://${fileName}`;
|
|
|
-
|
|
|
- hilog.info(0x0000, TAG, `📷 Local image copied successfully: ${realPath} -> ${tempFilePath}, fd: ${fd}`);
|
|
|
-
|
|
|
- const result: ProcessedImageInfo = {
|
|
|
- fileName,
|
|
|
- memoryUri,
|
|
|
- fd,
|
|
|
- sourceHash: cacheKey
|
|
|
- };
|
|
|
-
|
|
|
- // 添加到缓存
|
|
|
- this.localImageCache.set(cacheKey, result);
|
|
|
- hilog.info(0x0000, TAG, `📷 Added to local image cache: ${cacheKey}`);
|
|
|
-
|
|
|
- return result;
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- hilog.error(0x0000, TAG, `❌ Failed to process local image: ${error}`);
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检查是否为本地文件URI
|
|
|
- */
|
|
|
- private isLocalFileUri(url: string): boolean {
|
|
|
- return url.startsWith('file://');
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 清理本地图片缓存
|
|
|
- */
|
|
|
- private cleanupLocalImageCache(): void {
|
|
|
- const now = Date.now();
|
|
|
-
|
|
|
- // 每5分钟清理一次
|
|
|
- if (now - this.lastLocalCacheCleanup < 5 * 60 * 1000) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- this.lastLocalCacheCleanup = now;
|
|
|
-
|
|
|
- // 清理过期的缓存项
|
|
|
- const keysToDelete: string[] = [];
|
|
|
- const tempDir = this.appContext!.getApplicationContext().tempDir;
|
|
|
-
|
|
|
- this.localImageCache.forEach((info, key) => {
|
|
|
- const tempFilePath = `${tempDir}/${info.fileName}`;
|
|
|
-
|
|
|
- // 检查文件是否存在
|
|
|
- if (!fileIo.accessSync(tempFilePath)) {
|
|
|
- keysToDelete.push(key);
|
|
|
- hilog.info(0x0000, TAG, `📷 Removing cache entry for missing file: ${info.fileName}`);
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 如果缓存过大,删除最旧的项
|
|
|
- if (this.localImageCache.size > 20) {
|
|
|
- const sortedEntries = Array.from(this.localImageCache.entries())
|
|
|
- .sort((a, b) => {
|
|
|
- // 根据文件名中的时间戳排序
|
|
|
- const timeA = parseInt(a[1].fileName.split('_')[2] || '0');
|
|
|
- const timeB = parseInt(b[1].fileName.split('_')[2] || '0');
|
|
|
- return timeA - timeB;
|
|
|
- });
|
|
|
-
|
|
|
- const entriesToDelete = sortedEntries.slice(0, this.localImageCache.size - 15);
|
|
|
- entriesToDelete.forEach((entry) => {
|
|
|
- const key = entry[0];
|
|
|
- const info = entry[1];
|
|
|
- keysToDelete.push(key);
|
|
|
- // 尝试删除临时文件
|
|
|
- try {
|
|
|
- const filePath = `${tempDir}/${info.fileName}`;
|
|
|
- if (fileIo.accessSync(filePath)) {
|
|
|
- // 不要关闭可能已经无效的文件描述符,直接删除文件
|
|
|
- fileIo.unlinkSync(filePath);
|
|
|
- hilog.info(0x0000, TAG, `📷 Deleted old cached file: ${info.fileName}`);
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- hilog.warn(0x0000, TAG, `📷 Failed to delete cached file: ${info.fileName}`);
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 删除缓存项
|
|
|
- keysToDelete.forEach(key => {
|
|
|
- this.localImageCache.delete(key);
|
|
|
- });
|
|
|
-
|
|
|
- if (keysToDelete.length > 0) {
|
|
|
- hilog.info(0x0000, TAG, `📷 Cleaned up ${keysToDelete.length} local image cache entries`);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取文件扩展名
|
|
|
- */
|
|
|
- private getFileExtension(filePath: string): string | null {
|
|
|
- const lastDotIndex = filePath.lastIndexOf('.');
|
|
|
- if (lastDotIndex === -1 || lastDotIndex === filePath.length - 1) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- return filePath.substring(lastDotIndex + 1).toLowerCase();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理网络图片(缓存 + 下载)
|
|
|
- */
|
|
|
- private async handleNetworkImage(adaptedData: FormattedWidgetData, originalData: WidgetData): Promise<void> {
|
|
|
- if (!adaptedData.coverImage) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- // 如果已经是 memory:// 格式(本地图片已处理),验证缓存是否有效
|
|
|
- if (adaptedData.coverImage.startsWith('memory://')) {
|
|
|
- const fileName = adaptedData.coverImage.replace('memory://', '');
|
|
|
- const tempFilePath = `${this.appContext!.getApplicationContext().tempDir}/${fileName}`;
|
|
|
-
|
|
|
- // 验证文件是否存在
|
|
|
- if (fileIo.accessSync(tempFilePath)) {
|
|
|
- hilog.info(0x0000, TAG, `📷 Using existing memory URI: ${adaptedData.coverImage}`);
|
|
|
- adaptedData.imgName = fileName;
|
|
|
- return;
|
|
|
- } else {
|
|
|
- hilog.warn(0x0000, TAG, `📷 Memory URI file not found, will reprocess: ${fileName}`);
|
|
|
- // 清除无效的memory URI,继续处理
|
|
|
- adaptedData.coverImage = '';
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 处理网络图片
|
|
|
- if (!this.isNetworkUrl(adaptedData.coverImage)) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const imageUrl = adaptedData.coverImage;
|
|
|
-
|
|
|
-
|
|
|
- // 检查缓存
|
|
|
- const cachedItem = this.imageCache.get(imageUrl);
|
|
|
- if (cachedItem && cachedItem.expiry > Date.now()) {
|
|
|
-
|
|
|
- // 尝试打开缓存的文件获取文件描述符
|
|
|
- try {
|
|
|
- const cachedFilePath = `${this.appContext!.getApplicationContext().tempDir}/${cachedItem.fileName}`;
|
|
|
- const fileForWidget = fileIo.openSync(cachedFilePath, fileIo.OpenMode.READ_ONLY);
|
|
|
- const fd = fileForWidget.fd;
|
|
|
-
|
|
|
- adaptedData.coverImage = `memory://${cachedItem.fileName}`;
|
|
|
- adaptedData.imgName = cachedItem.fileName;
|
|
|
-
|
|
|
- // 设置文件描述符信息到原始数据
|
|
|
- originalData.imageFileInfo = {
|
|
|
- fileName: cachedItem.fileName,
|
|
|
- memoryUri: `memory://${cachedItem.fileName}`,
|
|
|
- fd: fd
|
|
|
- };
|
|
|
-
|
|
|
- hilog.info(0x0000, TAG, `📷 Using cached image with fd: ${fd}, fileName: ${cachedItem.fileName}`);
|
|
|
- // 注意:不要在这里关闭文件,文件描述符需要传递给卡片
|
|
|
- return;
|
|
|
- } catch (fdError) {
|
|
|
- hilog.warn(0x0000, TAG, `📷 Failed to open cached file for fd: ${fdError}, will redownload`);
|
|
|
- // 如果无法打开文件,继续下载新的
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 下载图片
|
|
|
- const downloadResult = await this.downloadAndCacheImage(imageUrl);
|
|
|
- if (downloadResult) {
|
|
|
- adaptedData.coverImage = `memory://${downloadResult.fileName}`;
|
|
|
- adaptedData.imgName = downloadResult.fileName;
|
|
|
-
|
|
|
- // 设置文件描述符信息到原始数据(memory协议需要)
|
|
|
- if (downloadResult.fd !== undefined) {
|
|
|
- originalData.imageFileInfo = {
|
|
|
- fileName: downloadResult.fileName,
|
|
|
- memoryUri: `memory://${downloadResult.fileName}`,
|
|
|
- fd: downloadResult.fd
|
|
|
- };
|
|
|
- hilog.info(0x0000, TAG, `📷 Network image downloaded with fd: ${downloadResult.fd}, fileName: ${downloadResult.fileName}`);
|
|
|
- }
|
|
|
-
|
|
|
- } else {
|
|
|
- // 下载失败,清除图片
|
|
|
- adaptedData.coverImage = '';
|
|
|
- adaptedData.imgName = '';
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- hilog.error(0x0000, TAG, `❌ Error processing network image: ${error}`);
|
|
|
- adaptedData.coverImage = '';
|
|
|
- adaptedData.imgName = '';
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 下载并缓存网络图片
|
|
|
- */
|
|
|
- private async downloadAndCacheImage(imageUrl: string): Promise<ImageDownloadResult | null> {
|
|
|
- // 检查是否正在下载
|
|
|
- if (this.downloadingImages.has(imageUrl)) {
|
|
|
-
|
|
|
- return await this.downloadingImages.get(imageUrl)!;
|
|
|
- }
|
|
|
-
|
|
|
- // 创建下载Promise
|
|
|
- const downloadPromise = this.performImageDownload(imageUrl);
|
|
|
- this.downloadingImages.set(imageUrl, downloadPromise);
|
|
|
-
|
|
|
- try {
|
|
|
- const result = await downloadPromise;
|
|
|
- return result;
|
|
|
- } finally {
|
|
|
- this.downloadingImages.delete(imageUrl);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 执行图片下载
|
|
|
- */
|
|
|
- private async performImageDownload(imageUrl: string): Promise<ImageDownloadResult | null> {
|
|
|
- try {
|
|
|
-
|
|
|
-
|
|
|
- const httpRequest = http.createHttp();
|
|
|
- const response = await httpRequest.request(imageUrl, {
|
|
|
- method: http.RequestMethod.GET,
|
|
|
- connectTimeout: 10000,
|
|
|
- readTimeout: 10000
|
|
|
- });
|
|
|
-
|
|
|
- if (response.responseCode !== 200) {
|
|
|
- throw new Error(`HTTP ${response.responseCode}`);
|
|
|
- }
|
|
|
-
|
|
|
- // 生成文件名
|
|
|
- const fileName = `widget_cover_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.jpg`;
|
|
|
-
|
|
|
- // 保存到临时目录(卡片要求使用tempDir)
|
|
|
- const filePath = `${this.appContext!.getApplicationContext().tempDir}/${fileName}`;
|
|
|
-
|
|
|
- // 修改点2: 显式声明buffer的类型为ArrayBuffer
|
|
|
- const buffer: ArrayBuffer = response.result as ArrayBuffer;
|
|
|
-
|
|
|
- // 根据官方文档:先创建文件并写入数据
|
|
|
- const tempFile = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
|
|
|
- try {
|
|
|
- // 使用 write 方法写入数据
|
|
|
- const writeLen = await fileIo.write(tempFile.fd, buffer);
|
|
|
- hilog.info(0x0000, TAG, `📷 Write data to file succeed and size is: ${writeLen}`);
|
|
|
- } catch (writeError) {
|
|
|
- hilog.error(0x0000, TAG, `❌ Write data to file failed: ${writeError}`);
|
|
|
- fileIo.closeSync(tempFile);
|
|
|
- httpRequest.destroy();
|
|
|
- return null;
|
|
|
- } finally {
|
|
|
- // 关闭临时文件句柄
|
|
|
- fileIo.closeSync(tempFile);
|
|
|
- }
|
|
|
-
|
|
|
- // 添加到缓存
|
|
|
- const cacheItem: ImageCacheItem = {
|
|
|
- fileName,
|
|
|
- downloadTime: Date.now(),
|
|
|
- expiry: Date.now() + this.imageCacheExpiry,
|
|
|
- fileSize: buffer.byteLength
|
|
|
- };
|
|
|
-
|
|
|
- this.imageCache.set(imageUrl, cacheItem);
|
|
|
- this.cleanupImageCache();
|
|
|
-
|
|
|
- // 根据官方文档:重新打开文件获取文件描述符用于卡片显示
|
|
|
- try {
|
|
|
- const fileForWidget = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
|
|
|
- const fd = fileForWidget.fd;
|
|
|
-
|
|
|
- hilog.info(0x0000, TAG, `📷 Image downloaded and opened for widget: ${fileName}, fd: ${fd}`);
|
|
|
-
|
|
|
- // 注意:根据官方文档,不要在这里关闭文件,文件描述符需要传递给卡片
|
|
|
- // fileIo.closeSync(fileForWidget); // 等待卡片更新完成后再关闭
|
|
|
-
|
|
|
- httpRequest.destroy();
|
|
|
- const result: ImageDownloadResult = { fileName: fileName, fd: fd };
|
|
|
- return result;
|
|
|
- } catch (fdError) {
|
|
|
- hilog.error(0x0000, TAG, `❌ Failed to open file for widget fd: ${fdError}`);
|
|
|
- httpRequest.destroy();
|
|
|
- const result: ImageDownloadResult = { fileName: fileName };
|
|
|
- return result; // 返回不包含fd的结果
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- hilog.error(0x0000, TAG, `❌ Failed to download image: ${error}`);
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 清理图片缓存
|
|
|
- */
|
|
|
- private cleanupImageCache(): void {
|
|
|
- if (this.imageCache.size <= this.maxCacheSize) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 按下载时间排序,删除最旧的
|
|
|
- const sortedItems = Array.from(this.imageCache.entries())
|
|
|
- .sort((a, b) => a[1].downloadTime - b[1].downloadTime);
|
|
|
-
|
|
|
- const itemsToDelete = sortedItems.slice(0, this.imageCache.size - this.maxCacheSize);
|
|
|
-
|
|
|
- for (let i = 0; i < itemsToDelete.length; i++) {
|
|
|
- try {
|
|
|
- const entry = itemsToDelete[i];
|
|
|
- const url = entry[0];
|
|
|
- const item = entry[1];
|
|
|
- const filePath = `${this.appContext!.cacheDir}/${item.fileName}`;
|
|
|
- if (fileIo.accessSync(filePath)) {
|
|
|
- fileIo.unlinkSync(filePath);
|
|
|
- }
|
|
|
- this.imageCache.delete(url);
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
-
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 初始化图片缓存
|
|
|
- */
|
|
|
- private initializeImageCache(): void {
|
|
|
- // 清理过期的缓存文件
|
|
|
- try {
|
|
|
- const cacheDir = this.appContext!.cacheDir;
|
|
|
- const files = fileIo.listFileSync(cacheDir);
|
|
|
-
|
|
|
- for (const file of files) {
|
|
|
- if (file.startsWith('widget_cover_')) {
|
|
|
- const filePath = `${cacheDir}/${file}`;
|
|
|
- const stat = fileIo.statSync(filePath);
|
|
|
-
|
|
|
- // 删除超过24小时的文件
|
|
|
- if (Date.now() - stat.mtime > this.imageCacheExpiry) {
|
|
|
- fileIo.unlinkSync(filePath);
|
|
|
-
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
-
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 预先验证卡片ID的有效性
|
|
|
- */
|
|
|
- private async preValidateFormIds(formIds: string[], prefs: preferences.Preferences): Promise<string[]> {
|
|
|
- const validFormIds: string[] = [];
|
|
|
- const invalidFormIds: string[] = [];
|
|
|
-
|
|
|
- hilog.info(0x0000, TAG, `🔍 Pre-validating ${formIds.length} form IDs...`);
|
|
|
-
|
|
|
- for (const formId of formIds) {
|
|
|
- try {
|
|
|
- // 尝试使用一个简单的测试数据来验证卡片ID
|
|
|
- const testData = formBindingData.createFormBindingData({
|
|
|
- test: 'validation'
|
|
|
- });
|
|
|
-
|
|
|
- // 尝试更新卡片,如果失败说明卡片ID无效
|
|
|
- await formProvider.updateForm(formId, testData);
|
|
|
- validFormIds.push(formId);
|
|
|
- hilog.debug(0x0000, TAG, `✅ [${formId}] Valid form ID`);
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- const errorStr :string= error.toString();
|
|
|
- if (errorStr.includes('form not exist') ||
|
|
|
- errorStr.includes('16501001') ||
|
|
|
- errorStr.includes('The ID of the form to be operated does not exist')) {
|
|
|
- hilog.warn(0x0000, TAG, `❌ [${formId}] Invalid form ID detected during validation`);
|
|
|
- invalidFormIds.push(formId);
|
|
|
- } else {
|
|
|
- // 其他错误,可能是临时性的,保留该ID
|
|
|
- hilog.warn(0x0000, TAG, `⚠️ [${formId}] Validation error (keeping): ${error}`);
|
|
|
- validFormIds.push(formId);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 清理无效的卡片ID
|
|
|
- if (invalidFormIds.length > 0) {
|
|
|
- hilog.info(0x0000, TAG, `🗑️ Cleaning up ${invalidFormIds.length} invalid form IDs`);
|
|
|
- for (const invalidFormId of invalidFormIds) {
|
|
|
- try {
|
|
|
- await this.preferencesUtil.removeFormId(prefs, invalidFormId);
|
|
|
- this.globalWidgetManager.unregisterWidget(invalidFormId);
|
|
|
- hilog.info(0x0000, TAG, `🗑️ [${invalidFormId}] Cleaned up invalid form ID`);
|
|
|
- } catch (cleanupError) {
|
|
|
- hilog.error(0x0000, TAG, `❌ [${invalidFormId}] Failed to cleanup: ${cleanupError}`);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- hilog.info(0x0000, TAG, `🔍 Validation complete: ${validFormIds.length} valid, ${invalidFormIds.length} invalid`);
|
|
|
- return validFormIds;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 验证活跃卡片与持久化卡片的一致性
|
|
|
- */
|
|
|
- private async validateActiveWidgets(formIds: string[], prefs: preferences.Preferences): Promise<void> {
|
|
|
- const activeWidgets = this.globalWidgetManager.getActiveWidgets();
|
|
|
- const activeFormIds = Array.from(activeWidgets.keys());
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- // 检查持久化但不活跃的卡片
|
|
|
- const persistentOnlyIds = formIds.filter(id => !activeWidgets.has(id));
|
|
|
- if (persistentOnlyIds.length > 0) {
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
- // 检查活跃但未持久化的卡片
|
|
|
- const activeOnlyIds = activeFormIds.filter(id => !formIds.includes(id));
|
|
|
- if (activeOnlyIds.length > 0) {
|
|
|
-
|
|
|
-
|
|
|
- // 将活跃的卡片添加到持久化存储
|
|
|
- for (const formId of activeOnlyIds) {
|
|
|
- await this.preferencesUtil.addFormId(prefs, formId);
|
|
|
-
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 保存增强的表单状态
|
|
|
- */
|
|
|
- private async saveEnhancedFormState(prefs: preferences.Preferences, formId: string, widgetSize: string, data: WidgetData): Promise<void> {
|
|
|
- try {
|
|
|
- const existingState = await this.preferencesUtil.getFormState(prefs, formId) as FormStateData | null;
|
|
|
- const updateCount = (existingState?.updateCount as number || 0) + 1;
|
|
|
-
|
|
|
- const stateData: FormStateData = {
|
|
|
- size: widgetSize,
|
|
|
- lastUpdate: Date.now(),
|
|
|
- lastSongId: data.currentSong.id,
|
|
|
- updateCount: updateCount,
|
|
|
- lastImageUrl: data.currentSong.coverImagePath || ''
|
|
|
- };
|
|
|
-
|
|
|
- await this.preferencesUtil.saveFormState(prefs, formId, stateData);
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
-
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理批量更新结果
|
|
|
- */
|
|
|
- private processBatchResults(formIds: string[], results: PromiseSettledResult<UpdateResult>[], startTime: number): BatchUpdateStats {
|
|
|
- const duration = Date.now() - startTime;
|
|
|
- let successCount = 0;
|
|
|
- let failedCount = 0;
|
|
|
- let totalUpdateTime = 0;
|
|
|
-
|
|
|
- results.forEach((result, index) => {
|
|
|
- const formId = formIds[index];
|
|
|
-
|
|
|
- if (result.status === 'fulfilled') {
|
|
|
- const updateResult = result.value;
|
|
|
- if (updateResult.success) {
|
|
|
- successCount++;
|
|
|
- totalUpdateTime += updateResult.updateTime;
|
|
|
-
|
|
|
- } else {
|
|
|
- failedCount++;
|
|
|
- hilog.error(0x0000, TAG, `❌ Form ${index + 1}/${formIds.length} (${formId}) failed: ${updateResult.error?.message}`);
|
|
|
- }
|
|
|
- } else {
|
|
|
- failedCount++;
|
|
|
- hilog.error(0x0000, TAG, `❌ Form ${index + 1}/${formIds.length} (${formId}) promise rejected: ${result.reason}`);
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- const averageUpdateTime = successCount > 0 ? totalUpdateTime / successCount : 0;
|
|
|
-
|
|
|
- return {
|
|
|
- total: formIds.length,
|
|
|
- success: successCount,
|
|
|
- failed: failedCount,
|
|
|
- duration,
|
|
|
- averageUpdateTime
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 清理无效的 Form ID(参考原有方法)
|
|
|
- */
|
|
|
- private async cleanupInvalidForms(prefs: preferences.Preferences, formIds: string[], results: PromiseSettledResult<UpdateResult>[]): Promise<void> {
|
|
|
- try {
|
|
|
- const invalidFormIds: string[] = [];
|
|
|
-
|
|
|
- results.forEach((result: PromiseSettledResult<UpdateResult>, index: number) => {
|
|
|
- if (result.status === 'fulfilled' && !result.value.success) {
|
|
|
- const error = result.value.error!;
|
|
|
- const errorStr = error.toString();
|
|
|
- const formId = formIds[index];
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- if (errorStr.includes('form not exist') ||
|
|
|
- errorStr.includes('16501001') ||
|
|
|
- errorStr.includes('FormProvider') ||
|
|
|
- errorStr.includes('invalid form')) {
|
|
|
-
|
|
|
- invalidFormIds.push(formId);
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- if (invalidFormIds.length > 0) {
|
|
|
-
|
|
|
-
|
|
|
- for (const invalidFormId of invalidFormIds) {
|
|
|
- await this.preferencesUtil.removeFormId(prefs, invalidFormId);
|
|
|
- this.globalWidgetManager.unregisterWidget(invalidFormId);
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- hilog.error(0x0000, TAG, `❌ Failed to cleanup invalid forms: ${error}`);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取更新统计信息
|
|
|
- */
|
|
|
- public getUpdateStats(): UpdateStats {
|
|
|
- const stats: UpdateStats = {
|
|
|
- totalUpdates: this.updateStats.totalUpdates,
|
|
|
- successfulUpdates: this.updateStats.successfulUpdates,
|
|
|
- failedUpdates: this.updateStats.failedUpdates,
|
|
|
- averageUpdateTime: this.updateStats.averageUpdateTime,
|
|
|
- lastBatchStats: this.updateStats.lastBatchStats
|
|
|
- };
|
|
|
- return stats;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 重置统计信息
|
|
|
- */
|
|
|
- public resetStats(): void {
|
|
|
- this.updateStats = {
|
|
|
- totalUpdates: 0,
|
|
|
- successfulUpdates: 0,
|
|
|
- failedUpdates: 0,
|
|
|
- averageUpdateTime: 0
|
|
|
- };
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 清理所有缓存
|
|
|
- */
|
|
|
- public clearAllCaches(): void {
|
|
|
- // 清理本地图片缓存
|
|
|
- this.localImageCache.forEach((info) => {
|
|
|
- try {
|
|
|
- const tempFilePath = `${this.appContext!.getApplicationContext().tempDir}/${info.fileName}`;
|
|
|
- if (fileIo.accessSync(tempFilePath)) {
|
|
|
- // 不要关闭可能已经无效的文件描述符,直接删除文件
|
|
|
- fileIo.unlinkSync(tempFilePath);
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- hilog.warn(0x0000, TAG, `Failed to clean cached file: ${info.fileName}`);
|
|
|
- }
|
|
|
- });
|
|
|
- this.localImageCache.clear();
|
|
|
- this.processingLocalImages.clear();
|
|
|
-
|
|
|
- // 清理网络图片缓存
|
|
|
- this.imageCache.clear();
|
|
|
- this.downloadingImages.clear();
|
|
|
-
|
|
|
- hilog.info(0x0000, TAG, '🧹 All caches cleared');
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 强制刷新所有卡片(忽略防抖)
|
|
|
- */
|
|
|
- public async forceUpdateAllForms(data: WidgetData): Promise<BatchUpdateStats> {
|
|
|
- this.lastUpdateTime = 0; // 重置防抖时间
|
|
|
- return await this.updateAllForms(data);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 工具方法
|
|
|
- */
|
|
|
- private isNetworkUrl(url: string): boolean {
|
|
|
- return url.startsWith('http://') || url.startsWith('https://');
|
|
|
- }
|
|
|
-
|
|
|
- private sleep(ms: number): Promise<void> {
|
|
|
- return new Promise(resolve => setTimeout(resolve, ms));
|
|
|
- }
|
|
|
-
|
|
|
- private createEmptyStats(): BatchUpdateStats {
|
|
|
- const emptyStats: BatchUpdateStats = {
|
|
|
- total: 0,
|
|
|
- success: 0,
|
|
|
- failed: 0,
|
|
|
- duration: 0,
|
|
|
- averageUpdateTime: 0
|
|
|
- };
|
|
|
- return emptyStats;
|
|
|
- }
|
|
|
-}
|