|
|
@@ -2,11 +2,13 @@ import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@
|
|
|
import { Configuration, Want } from '@kit.AbilityKit';
|
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
|
|
+import { fileIo } from '@kit.CoreFileKit';
|
|
|
+import { http } from '@kit.NetworkKit';
|
|
|
|
|
|
import { WidgetDataManager } from '../common/widget/WidgetDataManager';
|
|
|
import { PlayerControlService } from '../common/widget/PlayerControlService';
|
|
|
import { AvSessionWidgetListener } from '../common/widget/AvSessionWidgetListener';
|
|
|
-import { WidgetCommand, WidgetControlParams, WidgetData, WidgetSize } from '../common/widget/WidgetTypes';
|
|
|
+import { WidgetCommand, WidgetControlParams, WidgetData, WidgetSize, FormattedWidgetData } from '../common/widget/WidgetTypes';
|
|
|
import { FormLayoutManager } from '../common/widget/FormLayoutManager';
|
|
|
import { GlobalWidgetManager } from '../common/widget/GlobalWidgetManager';
|
|
|
import { WidgetSizeAdapter, SizeChangeListener } from '../common/widget/WidgetSizeAdapter';
|
|
|
@@ -53,6 +55,39 @@ interface WidgetConfigParams extends Record<string, Object> {
|
|
|
configValue: string;
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 扩展的卡片数据接口,支持图片传递
|
|
|
+ */
|
|
|
+interface ExtendedWidgetData extends FormattedWidgetData {
|
|
|
+ formImages?: Record<string, number>;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 自定义对象复制函数,替代Object.assign
|
|
|
+ */
|
|
|
+function copyWidgetData(target: ExtendedWidgetData, overrides: Partial<ExtendedWidgetData>): ExtendedWidgetData {
|
|
|
+ return {
|
|
|
+ isPlaying: overrides.isPlaying !== undefined ? overrides.isPlaying : target.isPlaying,
|
|
|
+ isPaused: overrides.isPaused !== undefined ? overrides.isPaused : target.isPaused,
|
|
|
+ isLoading: overrides.isLoading !== undefined ? overrides.isLoading : target.isLoading,
|
|
|
+ songTitle: overrides.songTitle !== undefined ? overrides.songTitle : target.songTitle,
|
|
|
+ songArtist: overrides.songArtist !== undefined ? overrides.songArtist : target.songArtist,
|
|
|
+ songAlbum: overrides.songAlbum !== undefined ? overrides.songAlbum : target.songAlbum,
|
|
|
+ coverImage: overrides.coverImage !== undefined ? overrides.coverImage : target.coverImage,
|
|
|
+ currentTime: overrides.currentTime !== undefined ? overrides.currentTime : target.currentTime,
|
|
|
+ totalTime: overrides.totalTime !== undefined ? overrides.totalTime : target.totalTime,
|
|
|
+ progressPercentage: overrides.progressPercentage !== undefined ? overrides.progressPercentage : target.progressPercentage,
|
|
|
+ hasNext: overrides.hasNext !== undefined ? overrides.hasNext : target.hasNext,
|
|
|
+ hasPrevious: overrides.hasPrevious !== undefined ? overrides.hasPrevious : target.hasPrevious,
|
|
|
+ showProgress: overrides.showProgress !== undefined ? overrides.showProgress : target.showProgress,
|
|
|
+ showCover: overrides.showCover !== undefined ? overrides.showCover : target.showCover,
|
|
|
+ widgetSize: overrides.widgetSize !== undefined ? overrides.widgetSize : target.widgetSize,
|
|
|
+ timestamp: overrides.timestamp !== undefined ? overrides.timestamp : target.timestamp,
|
|
|
+ imgName: overrides.imgName !== undefined ? overrides.imgName : target.imgName,
|
|
|
+ formImages: overrides.formImages !== undefined ? overrides.formImages : target.formImages
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 桌面播放器卡片扩展能力
|
|
|
* 负责处理卡片的生命周期管理和用户交互事件
|
|
|
@@ -79,6 +114,10 @@ implements SizeChangeListener, ThemeChangeListener, PreferencesChangeListener {
|
|
|
// 进程启动时间,用于检测进程重启
|
|
|
private processStartTime: number = Date.now();
|
|
|
private lastHealthCheck: number = 0;
|
|
|
+
|
|
|
+ // 图片下载缓存
|
|
|
+ private downloadingImages: Map<string, Promise<string | null>> = new Map();
|
|
|
+ private imageCache: Map<string, string> = new Map(); // url -> fileName映射
|
|
|
|
|
|
/**
|
|
|
* 初始化服务
|
|
|
@@ -196,29 +235,44 @@ implements SizeChangeListener, ThemeChangeListener, PreferencesChangeListener {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 更新单个卡片,带重试机制
|
|
|
+ * 更新单个卡片,带重试机制和网络图片支持
|
|
|
*/
|
|
|
private updateSingleWidget(formId: string, size: WidgetSize, data: WidgetData, retryCount: number = 0): void {
|
|
|
try {
|
|
|
// 根据卡片尺寸适配数据
|
|
|
- const adaptedData = this.layoutManager.adaptDataForSize(data, size);
|
|
|
+ const formattedData = this.layoutManager.adaptDataForSize(data, size);
|
|
|
+
|
|
|
+ // 转换为ExtendedWidgetData
|
|
|
+ const adaptedData: ExtendedWidgetData = {
|
|
|
+ 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: formattedData.formImages
|
|
|
+ };
|
|
|
|
|
|
hilog.info(0x0000, TAG, `Heanup widget ${formId} calling formProvider.updateForm with isPlaying=${adaptedData.isPlaying}, hasNext=${adaptedData.hasNext}, hasPrevious=${adaptedData.hasPrevious}, coverImage=${adaptedData.coverImage || 'empty'}`);
|
|
|
|
|
|
- // 直接使用系统API更新卡片
|
|
|
- const formData = formBindingData.createFormBindingData(adaptedData);
|
|
|
- formProvider.updateForm(formId, formData).then(() => {
|
|
|
- hilog.info(0x0000, TAG, `Heanup widget ${formId} updated successfully: isPlaying=${adaptedData.isPlaying}, title=${adaptedData.songTitle}, hasNext=${adaptedData.hasNext}, hasPrevious=${adaptedData.hasPrevious}`);
|
|
|
- }).catch(() => {
|
|
|
- hilog.error(0x0000, TAG, `Heanup widget ${formId} update failed, retry count: ${retryCount}`);
|
|
|
-
|
|
|
- // 重试机制:最多重试2次
|
|
|
- if (retryCount < 2) {
|
|
|
- setTimeout(() => {
|
|
|
- this.updateSingleWidget(formId, size, data, retryCount + 1);
|
|
|
- }, 1000 * (retryCount + 1)); // 递增延迟
|
|
|
- }
|
|
|
- });
|
|
|
+ // 检查是否有网络图片需要下载
|
|
|
+ if (adaptedData.coverImage && this.isNetworkUrl(adaptedData.coverImage)) {
|
|
|
+ this.updateWidgetWithNetworkImage(formId, adaptedData, retryCount);
|
|
|
+ } else {
|
|
|
+ // 没有网络图片,直接更新
|
|
|
+ this.updateWidgetDirectly(formId, adaptedData, retryCount);
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
hilog.error(0x0000, TAG, `Heanup widget ${formId} process error: ${error}`);
|
|
|
|
|
|
@@ -231,6 +285,178 @@ implements SizeChangeListener, ThemeChangeListener, PreferencesChangeListener {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 直接更新卡片(无网络图片)
|
|
|
+ */
|
|
|
+ private updateWidgetDirectly(formId: string, adaptedData: ExtendedWidgetData, retryCount: number = 0): void {
|
|
|
+ const formData = formBindingData.createFormBindingData(adaptedData);
|
|
|
+ formProvider.updateForm(formId, formData).then(() => {
|
|
|
+ hilog.info(0x0000, TAG, `Heanup widget ${formId} updated successfully: isPlaying=${adaptedData.isPlaying}, title=${adaptedData.songTitle}, hasNext=${adaptedData.hasNext}, hasPrevious=${adaptedData.hasPrevious}`);
|
|
|
+ }).catch(() => {
|
|
|
+ hilog.error(0x0000, TAG, `Heanup widget ${formId} update failed, retry count: ${retryCount}`);
|
|
|
+
|
|
|
+ // 重试机制:最多重试2次
|
|
|
+ if (retryCount < 2) {
|
|
|
+ setTimeout(() => {
|
|
|
+ this.updateWidgetDirectly(formId, adaptedData, retryCount + 1);
|
|
|
+ }, 1000 * (retryCount + 1)); // 递增延迟
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 使用网络图片更新卡片
|
|
|
+ */
|
|
|
+ private async updateWidgetWithNetworkImage(formId: string, adaptedData: ExtendedWidgetData, retryCount: number = 0): Promise<void> {
|
|
|
+ try {
|
|
|
+ const imageUrl: string = adaptedData.coverImage;
|
|
|
+ hilog.info(0x0000, TAG, `Heanup widget ${formId} downloading network image: ${imageUrl}`);
|
|
|
+
|
|
|
+ // 先用无图片的数据快速更新一次
|
|
|
+ const dataWithoutImage: ExtendedWidgetData = copyWidgetData(adaptedData, { coverImage: '' });
|
|
|
+ this.updateWidgetDirectly(formId, dataWithoutImage, 0);
|
|
|
+
|
|
|
+ // 下载图片
|
|
|
+ const fileName = await this.downloadNetworkImage(imageUrl);
|
|
|
+
|
|
|
+ if (fileName) {
|
|
|
+ // 准备包含图片的数据
|
|
|
+ const imageMap: Record<string, number> = {};
|
|
|
+ imageMap[fileName] = await this.getImageFileDescriptor(fileName);
|
|
|
+
|
|
|
+ const dataWithImage: ExtendedWidgetData = copyWidgetData(adaptedData, {
|
|
|
+ coverImage: '', // 清空URL
|
|
|
+ imgName: fileName, // 设置图片名称用于memory://协议
|
|
|
+ formImages: imageMap
|
|
|
+ });
|
|
|
+
|
|
|
+ const formDataWithImage = formBindingData.createFormBindingData(dataWithImage);
|
|
|
+ await formProvider.updateForm(formId, formDataWithImage);
|
|
|
+
|
|
|
+ hilog.info(0x0000, TAG, `Heanup widget ${formId} updated with network image: ${fileName}`);
|
|
|
+ } else {
|
|
|
+ hilog.warn(0x0000, TAG, `Heanup widget ${formId} failed to download image, using default`);
|
|
|
+ // 图片下载失败,使用默认图片
|
|
|
+ this.updateWidgetDirectly(formId, dataWithoutImage, 0);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ hilog.error(0x0000, TAG, `Heanup widget ${formId} network image update error: ${error}`);
|
|
|
+
|
|
|
+ // 失败后回退到无图片模式
|
|
|
+ const dataWithoutImage: ExtendedWidgetData = copyWidgetData(adaptedData, { coverImage: '' });
|
|
|
+ this.updateWidgetDirectly(formId, dataWithoutImage, retryCount);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查是否为网络URL
|
|
|
+ */
|
|
|
+ private isNetworkUrl(url: string): boolean {
|
|
|
+ return url.startsWith('http://') || url.startsWith('https://');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 下载网络图片
|
|
|
+ */
|
|
|
+ private async downloadNetworkImage(imageUrl: string): Promise<string | null> {
|
|
|
+ try {
|
|
|
+ // 检查缓存
|
|
|
+ if (this.imageCache.has(imageUrl)) {
|
|
|
+ const fileName = this.imageCache.get(imageUrl)!;
|
|
|
+ hilog.info(0x0000, TAG, `Using cached image: ${fileName} for ${imageUrl}`);
|
|
|
+ return fileName;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否正在下载
|
|
|
+ if (this.downloadingImages.has(imageUrl)) {
|
|
|
+ hilog.info(0x0000, TAG, `Image already downloading, waiting: ${imageUrl}`);
|
|
|
+ const existingPromise = this.downloadingImages.get(imageUrl);
|
|
|
+ if (existingPromise) {
|
|
|
+ return await existingPromise;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开始下载
|
|
|
+ const downloadPromise = this.performImageDownload(imageUrl);
|
|
|
+ this.downloadingImages.set(imageUrl, downloadPromise);
|
|
|
+
|
|
|
+ const fileName = await downloadPromise;
|
|
|
+
|
|
|
+ // 清理下载状态
|
|
|
+ this.downloadingImages.delete(imageUrl);
|
|
|
+
|
|
|
+ if (fileName) {
|
|
|
+ this.imageCache.set(imageUrl, fileName);
|
|
|
+ }
|
|
|
+
|
|
|
+ return fileName;
|
|
|
+ } catch (error) {
|
|
|
+ hilog.error(0x0000, TAG, `Failed to download image ${imageUrl}: ${error}`);
|
|
|
+ this.downloadingImages.delete(imageUrl);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行图片下载
|
|
|
+ */
|
|
|
+ private async performImageDownload(imageUrl: string): Promise<string | null> {
|
|
|
+ try {
|
|
|
+ // 创建HTTP请求
|
|
|
+ const httpRequest = http.createHttp();
|
|
|
+
|
|
|
+ // 设置超时时间为3秒,适合卡片的快速响应需求
|
|
|
+ const response = await httpRequest.request(imageUrl, {
|
|
|
+ method: http.RequestMethod.GET,
|
|
|
+ expectDataType: http.HttpDataType.ARRAY_BUFFER,
|
|
|
+ connectTimeout: 3000,
|
|
|
+ readTimeout: 3000
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.responseCode === http.ResponseCode.OK && response.result) {
|
|
|
+ // 生成文件名
|
|
|
+ const fileName = 'cover_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
|
|
|
+ const tempDir = this.context.getApplicationContext().tempDir;
|
|
|
+ const filePath = tempDir + '/' + fileName;
|
|
|
+
|
|
|
+ // 保存文件
|
|
|
+ const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
|
|
|
+ await fileIo.write(file.fd, response.result as ArrayBuffer);
|
|
|
+ fileIo.closeSync(file);
|
|
|
+
|
|
|
+ hilog.info(0x0000, TAG, `Image downloaded successfully: ${fileName}`);
|
|
|
+ httpRequest.destroy();
|
|
|
+ return fileName;
|
|
|
+ } else {
|
|
|
+ hilog.error(0x0000, TAG, `HTTP request failed: ${response.responseCode}`);
|
|
|
+ httpRequest.destroy();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ hilog.error(0x0000, TAG, `Download error: ${error}`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取图片文件描述符
|
|
|
+ */
|
|
|
+ private async getImageFileDescriptor(fileName: string): Promise<number> {
|
|
|
+ try {
|
|
|
+ const tempDir = this.context.getApplicationContext().tempDir;
|
|
|
+ const filePath = tempDir + '/' + fileName;
|
|
|
+ const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
|
|
|
+
|
|
|
+ // 注意:文件描述符会被系统自动管理,不需要手动关闭
|
|
|
+ // 系统会在卡片更新完成后自动关闭文件描述符
|
|
|
+ hilog.info(0x0000, TAG, `Got file descriptor for ${fileName}: ${file.fd}`);
|
|
|
+ return file.fd;
|
|
|
+ } catch (error) {
|
|
|
+ hilog.error(0x0000, TAG, `Failed to get file descriptor for ${fileName}: ${error}`);
|
|
|
+ throw new Error(`Failed to get file descriptor for ${fileName}: ${error}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 卡片创建时调用
|
|
|
*/
|