|
@@ -0,0 +1,673 @@
|
|
|
|
|
+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 } 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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 更新统计接口
|
|
|
|
|
+ */
|
|
|
|
|
+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<string | null>> = new Map();
|
|
|
|
|
+ private readonly imageCacheExpiry: number = 24 * 60 * 60 * 1000; // 24小时
|
|
|
|
|
+ private readonly maxCacheSize: number = 50; // 最多缓存50张图片
|
|
|
|
|
+
|
|
|
|
|
+ // 性能统计
|
|
|
|
|
+ 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;
|
|
|
|
|
+ hilog.info(0x0000, TAG, '🎯 App context set for EnhancedFormUpdateService');
|
|
|
|
|
+ this.initializeImageCache();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 主要的更新所有卡片方法 - 参考原DirectFormUpdateService.updateAllForms()
|
|
|
|
|
+ */
|
|
|
|
|
+ public async updateAllForms(data: WidgetData): Promise<BatchUpdateStats> {
|
|
|
|
|
+ const batchStartTime = Date.now();
|
|
|
|
|
+
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `⏭️ Update debounced, skipping (${now - this.lastUpdateTime}ms since last update)`);
|
|
|
|
|
+ return this.createEmptyStats();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 防止并发更新
|
|
|
|
|
+ if (this.isUpdating) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, '⚠️ Update already in progress, skipping this request');
|
|
|
|
|
+ return this.createEmptyStats();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.isUpdating = true;
|
|
|
|
|
+ this.lastUpdateTime = now;
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🚀 Starting enhanced batch form update: isPlaying=${data.playState.isPlaying}, title="${data.currentSong.title}"`);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取所有持久化的 Form ID(使用原有方法)
|
|
|
|
|
+ const prefs = await this.preferencesUtil.getPreferences(this.appContext);
|
|
|
|
|
+ const formIds = await this.preferencesUtil.getFormIds(prefs);
|
|
|
|
|
+
|
|
|
|
|
+ if (formIds.length === 0) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, '📋 No forms found in persistence, skipping update');
|
|
|
|
|
+ return this.createEmptyStats();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `📋 Found ${formIds.length} forms to update: [${formIds.join(', ')}]`);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证活跃卡片与持久化卡片的一致性
|
|
|
|
|
+ await this.validateActiveWidgets(formIds, prefs);
|
|
|
|
|
+
|
|
|
|
|
+ // 并行更新所有卡片
|
|
|
|
|
+ const updatePromises = formIds.map((formId, index) => {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🎯 Creating update task ${index + 1}/${formIds.length} for form: ${formId}`);
|
|
|
|
|
+ return this.updateSingleFormWithRetry(formId, data, prefs);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `⏳ Executing ${updatePromises.length} parallel update tasks...`);
|
|
|
|
|
+
|
|
|
|
|
+ // 等待所有更新完成
|
|
|
|
|
+ const results = await Promise.allSettled(updatePromises);
|
|
|
|
|
+
|
|
|
|
|
+ // 处理结果并生成统计信息
|
|
|
|
|
+ const batchStats = this.processBatchResults(formIds, results, batchStartTime);
|
|
|
|
|
+
|
|
|
|
|
+ // 清理无效的 Form ID
|
|
|
|
|
+ if (batchStats.failed > 0) {
|
|
|
|
|
+ await this.cleanupInvalidForms(prefs, formIds, results);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新统计信息
|
|
|
|
|
+ this.updateStats.lastBatchStats = batchStats;
|
|
|
|
|
+ this.updateStats.totalUpdates += batchStats.total;
|
|
|
|
|
+ this.updateStats.successfulUpdates += batchStats.success;
|
|
|
|
|
+ this.updateStats.failedUpdates += batchStats.failed;
|
|
|
|
|
+
|
|
|
|
|
+ // 重新计算平均更新时间
|
|
|
|
|
+ if (this.updateStats.totalUpdates > 0) {
|
|
|
|
|
+ this.updateStats.averageUpdateTime =
|
|
|
|
|
+ (this.updateStats.averageUpdateTime * (this.updateStats.totalUpdates - batchStats.total) + batchStats.averageUpdateTime * batchStats.total) / this.updateStats.totalUpdates;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `✅ Batch update completed: ${batchStats.success}/${batchStats.total} successful in ${batchStats.duration}ms (avg: ${batchStats.averageUpdateTime.toFixed(1)}ms per form)`);
|
|
|
|
|
+
|
|
|
|
|
+ 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)];
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🔄 [${formId}] Retry attempt ${attempt}/${this.maxRetryCount} after ${delay}ms delay`);
|
|
|
|
|
+ await this.sleep(delay);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await this.updateSingleForm(formId, data, prefs);
|
|
|
|
|
+
|
|
|
|
|
+ const updateTime = Date.now() - startTime;
|
|
|
|
|
+ hilog.info(0x0000, TAG, `✅ [${formId}] Update successful on attempt ${attempt + 1} (${updateTime}ms)`);
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ formId,
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ updateTime,
|
|
|
|
|
+ retryCount: attempt
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ lastError = error as Error;
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `⚠️ [${formId}] Update attempt ${attempt + 1} failed: ${lastError.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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 {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🎯 [${formId}] Starting enhanced form update...`);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取卡片的当前状态
|
|
|
|
|
+ const formState = await this.preferencesUtil.getFormState(prefs, formId);
|
|
|
|
|
+ const widgetSizeStr = (formState?.size as string) || 'medium';
|
|
|
|
|
+ const widgetSize: WidgetSize = widgetSizeStr as WidgetSize;
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🎯 [${formId}] Widget size: ${widgetSizeStr}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 适配数据到卡片尺寸
|
|
|
|
|
+ const adaptedData = this.layoutManager.adaptDataForSize(data, widgetSize);
|
|
|
|
|
+
|
|
|
|
|
+ // 处理网络图片
|
|
|
|
|
+ await this.handleNetworkImage(adaptedData);
|
|
|
|
|
+
|
|
|
|
|
+ // 转换为 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 || ''
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🎯 [${formId}] Formatted data prepared: isPlaying=${formattedData.isPlaying}, title="${formattedData.songTitle}"`);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建 FormBindingData 并更新卡片
|
|
|
|
|
+ const formData = formBindingData.createFormBindingData(formattedData);
|
|
|
|
|
+ await formProvider.updateForm(formId, formData);
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `✅ [${formId}] Form updated successfully`);
|
|
|
|
|
+
|
|
|
|
|
+ // 保存增强状态信息
|
|
|
|
|
+ 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}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理网络图片(缓存 + 下载)
|
|
|
|
|
+ */
|
|
|
|
|
+ private async handleNetworkImage(adaptedData: FormattedWidgetData): Promise<void> {
|
|
|
|
|
+ if (!adaptedData.coverImage || !this.isNetworkUrl(adaptedData.coverImage)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const imageUrl = adaptedData.coverImage;
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🖼️ Processing network image: ${imageUrl}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查缓存
|
|
|
|
|
+ const cachedItem = this.imageCache.get(imageUrl);
|
|
|
|
|
+ if (cachedItem && cachedItem.expiry > Date.now()) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `📸 Using cached image: ${cachedItem.fileName}`);
|
|
|
|
|
+ adaptedData.coverImage = `memory://${cachedItem.fileName}`;
|
|
|
|
|
+ adaptedData.imgName = cachedItem.fileName;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 下载图片
|
|
|
|
|
+ const fileName = await this.downloadAndCacheImage(imageUrl);
|
|
|
|
|
+ if (fileName) {
|
|
|
|
|
+ adaptedData.coverImage = `memory://${fileName}`;
|
|
|
|
|
+ adaptedData.imgName = fileName;
|
|
|
|
|
+ hilog.info(0x0000, TAG, `📸 Network image processed: ${fileName}`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 下载失败,清除图片
|
|
|
|
|
+ adaptedData.coverImage = '';
|
|
|
|
|
+ adaptedData.imgName = '';
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `⚠️ Failed to download image, using default`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `❌ Error processing network image: ${error}`);
|
|
|
|
|
+ adaptedData.coverImage = '';
|
|
|
|
|
+ adaptedData.imgName = '';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 下载并缓存网络图片
|
|
|
|
|
+ */
|
|
|
|
|
+ private async downloadAndCacheImage(imageUrl: string): Promise<string | null> {
|
|
|
|
|
+ // 检查是否正在下载
|
|
|
|
|
+ if (this.downloadingImages.has(imageUrl)) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `⏳ Image already downloading: ${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<string | null> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `📥 Downloading image: ${imageUrl}`);
|
|
|
|
|
+
|
|
|
|
|
+ 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`;
|
|
|
|
|
+
|
|
|
|
|
+ // 保存到缓存目录
|
|
|
|
|
+ const filePath = `${this.appContext!.cacheDir}/${fileName}`;
|
|
|
|
|
+ const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
|
|
|
|
|
+
|
|
|
|
|
+ const buffer = response.result as ArrayBuffer;
|
|
|
|
|
+ fileIo.writeSync(file.fd, new Uint8Array(buffer));
|
|
|
|
|
+ fileIo.closeSync(file);
|
|
|
|
|
+
|
|
|
|
|
+ // 添加到缓存
|
|
|
|
|
+ const cacheItem: ImageCacheItem = {
|
|
|
|
|
+ fileName,
|
|
|
|
|
+ downloadTime: Date.now(),
|
|
|
|
|
+ expiry: Date.now() + this.imageCacheExpiry,
|
|
|
|
|
+ fileSize: buffer.byteLength
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.imageCache.set(imageUrl, cacheItem);
|
|
|
|
|
+ this.cleanupImageCache();
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `✅ Image downloaded and cached: ${fileName} (${buffer.byteLength} bytes)`);
|
|
|
|
|
+
|
|
|
|
|
+ httpRequest.destroy();
|
|
|
|
|
+ return fileName;
|
|
|
|
|
+
|
|
|
|
|
+ } 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);
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🗑️ Cleaned up cached image: ${item.fileName}`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `⚠️ Failed to cleanup image: ${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);
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🗑️ Cleaned up expired cache file: ${file}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `⚠️ Failed to cleanup cache directory: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 验证活跃卡片与持久化卡片的一致性
|
|
|
|
|
+ */
|
|
|
|
|
+ private async validateActiveWidgets(formIds: string[], prefs: preferences.Preferences): Promise<void> {
|
|
|
|
|
+ const activeWidgets = this.globalWidgetManager.getActiveWidgets();
|
|
|
|
|
+ const activeFormIds = Array.from(activeWidgets.keys());
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🔍 Validating widgets - Persistent: [${formIds.join(', ')}], Active: [${activeFormIds.join(', ')}]`);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查持久化但不活跃的卡片
|
|
|
|
|
+ const persistentOnlyIds = formIds.filter(id => !activeWidgets.has(id));
|
|
|
|
|
+ if (persistentOnlyIds.length > 0) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `⚠️ Found ${persistentOnlyIds.length} persistent but inactive widgets: [${persistentOnlyIds.join(', ')}]`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查活跃但未持久化的卡片
|
|
|
|
|
+ const activeOnlyIds = activeFormIds.filter(id => !formIds.includes(id));
|
|
|
|
|
+ if (activeOnlyIds.length > 0) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `⚠️ Found ${activeOnlyIds.length} active but not persistent widgets: [${activeOnlyIds.join(', ')}]`);
|
|
|
|
|
+
|
|
|
|
|
+ // 将活跃的卡片添加到持久化存储
|
|
|
|
|
+ for (const formId of activeOnlyIds) {
|
|
|
|
|
+ await this.preferencesUtil.addFormId(prefs, formId);
|
|
|
|
|
+ hilog.info(0x0000, TAG, `➕ Added missing form ID to persistence: ${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);
|
|
|
|
|
+ hilog.info(0x0000, TAG, `💾 [${formId}] Enhanced form state saved (update #${updateCount})`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `⚠️ [${formId}] Failed to save enhanced form state: ${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;
|
|
|
|
|
+ hilog.info(0x0000, TAG, `✅ Form ${index + 1}/${formIds.length} (${formId}) updated successfully in ${updateResult.updateTime}ms (${updateResult.retryCount} retries)`);
|
|
|
|
|
+ } 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];
|
|
|
|
|
+
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `🔍 Analyzing error for form ${formId}: ${errorStr}`);
|
|
|
|
|
+
|
|
|
|
|
+ if (errorStr.includes('form not exist') ||
|
|
|
|
|
+ errorStr.includes('16501001') ||
|
|
|
|
|
+ errorStr.includes('FormProvider') ||
|
|
|
|
|
+ errorStr.includes('invalid form')) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `🗑️ Form ${formId} appears to be invalid, marking for cleanup`);
|
|
|
|
|
+ invalidFormIds.push(formId);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (invalidFormIds.length > 0) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🧹 Cleaning up ${invalidFormIds.length} invalid forms: [${invalidFormIds.join(', ')}]`);
|
|
|
|
|
+
|
|
|
|
|
+ for (const invalidFormId of invalidFormIds) {
|
|
|
|
|
+ await this.preferencesUtil.removeFormId(prefs, invalidFormId);
|
|
|
|
|
+ this.globalWidgetManager.unregisterWidget(invalidFormId);
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🗑️ Removed invalid form ID: ${invalidFormId}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `✅ Invalid forms cleaned up successfully`);
|
|
|
|
|
+ }
|
|
|
|
|
+ } 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
|
|
|
|
|
+ };
|
|
|
|
|
+ hilog.info(0x0000, TAG, '📊 Update statistics reset');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 强制刷新所有卡片(忽略防抖)
|
|
|
|
|
+ */
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|