WidgetDataManager.ets 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. import formProvider from '@ohos.app.form.formProvider';
  2. import formBindingData from '@ohos.app.form.formBindingData';
  3. import preferences from '@ohos.data.preferences';
  4. import { hilog } from '@kit.PerformanceAnalysisKit';
  5. import { WidgetData, WidgetSize, WidgetTheme, FormattedWidgetData, PreferencesData, CacheStats } from './WidgetTypes';
  6. const TAG = 'WidgetDataManager';
  7. const WIDGET_PREFERENCES_NAME = 'widget_data_prefs';
  8. const CACHE_EXPIRY_TIME = 30 * 1000; // 30秒缓存过期时间
  9. /**
  10. * 缓存项接口
  11. */
  12. interface CacheItem {
  13. data: WidgetData;
  14. timestamp: number;
  15. expiry: number;
  16. }
  17. /**
  18. * 卡片数据管理器
  19. * 负责卡片数据的持久化存储、缓存和更新
  20. */
  21. export class WidgetDataManager {
  22. private preferencesStore: preferences.Preferences | null = null;
  23. private dataCache: Map<string, CacheItem> = new Map();
  24. private lastUpdateTime: number = 0;
  25. private context: object | null = null;
  26. constructor(context?: object) {
  27. this.context = context || null;
  28. this.initPreferences();
  29. this.startCacheCleanup();
  30. }
  31. /**
  32. * 初始化数据存储
  33. */
  34. private async initPreferences(): Promise<void> {
  35. try {
  36. if (this.context) {
  37. const store = await preferences.getPreferences(this.context as Context, WIDGET_PREFERENCES_NAME);
  38. this.preferencesStore = store;
  39. hilog.info(0x0000, TAG, 'Preferences initialized successfully with context');
  40. } else {
  41. hilog.warn(0x0000, TAG, 'No context provided, preferences initialization skipped');
  42. }
  43. } catch (error) {
  44. hilog.error(0x0000, TAG, `Failed to initialize preferences: ${error}`);
  45. }
  46. }
  47. /**
  48. * 获取初始卡片数据
  49. */
  50. getInitialWidgetData(): WidgetData {
  51. const initialData: WidgetData = {
  52. playState: {
  53. isPlaying: false,
  54. isPaused: true,
  55. isLoading: false
  56. },
  57. currentSong: {
  58. id: '',
  59. title: '暂无播放',
  60. artist: '未知艺术家',
  61. album: '未知专辑',
  62. coverImagePath: '',
  63. duration: 0
  64. },
  65. progress: {
  66. currentPosition: 0,
  67. duration: 0,
  68. percentage: 0,
  69. currentTimeText: '00:00',
  70. totalTimeText: '00:00'
  71. },
  72. playlist: {
  73. hasNext: false,
  74. hasPrevious: false,
  75. currentIndex: 0,
  76. totalCount: 0
  77. },
  78. config: {
  79. size: WidgetSize.MEDIUM,
  80. theme: WidgetTheme.AUTO,
  81. showProgress: true,
  82. showCover: true
  83. }
  84. };
  85. return initialData;
  86. }
  87. /**
  88. * 保存卡片数据
  89. */
  90. async saveWidgetData(formId: string, data: WidgetData | FormattedWidgetData): Promise<void> {
  91. try {
  92. if (!this.preferencesStore) {
  93. await this.initPreferences();
  94. }
  95. const dataKey = `widget_${formId}`;
  96. await this.preferencesStore?.put(dataKey, JSON.stringify(data));
  97. await this.preferencesStore?.flush();
  98. hilog.info(0x0000, TAG, `Widget data saved for form: ${formId}`);
  99. } catch (error) {
  100. hilog.error(0x0000, TAG, `Failed to save widget data: ${error}`);
  101. }
  102. }
  103. /**
  104. * 获取卡片数据
  105. */
  106. async getWidgetData(formId: string): Promise<WidgetData> {
  107. try {
  108. if (!this.preferencesStore) {
  109. await this.initPreferences();
  110. }
  111. const dataKey = `widget_${formId}`;
  112. const dataStr = await this.preferencesStore?.get(dataKey, '') as string;
  113. if (dataStr) {
  114. return JSON.parse(dataStr) as WidgetData;
  115. } else {
  116. return this.getInitialWidgetData() as WidgetData;
  117. }
  118. } catch (error) {
  119. hilog.error(0x0000, TAG, `Failed to get widget data: ${error}`);
  120. return this.getInitialWidgetData() as WidgetData;
  121. }
  122. }
  123. /**
  124. * 更新卡片显示
  125. */
  126. async updateWidget(formId: string, data: WidgetData | FormattedWidgetData): Promise<void> {
  127. try {
  128. hilog.info(0x0000, TAG, `Updating widget ${formId} with data type check`);
  129. let formattedData: FormattedWidgetData;
  130. // 检查数据类型,如果已经是格式化数据则直接使用
  131. if (this.isFormattedWidgetData(data)) {
  132. hilog.info(0x0000, TAG, `Data is already formatted for widget ${formId}`);
  133. formattedData = data as FormattedWidgetData;
  134. // 如果是格式化数据,需要转换回WidgetData进行存储
  135. const widgetData = this.convertToWidgetData(formattedData);
  136. await this.saveWidgetData(formId, widgetData);
  137. } else {
  138. hilog.info(0x0000, TAG, `Formatting raw data for widget ${formId}`);
  139. // 保存原始数据到本地存储
  140. await this.saveWidgetData(formId, data as WidgetData);
  141. // 格式化数据用于卡片显示
  142. formattedData = this.formatDataForWidget(data as WidgetData);
  143. }
  144. hilog.info(0x0000, TAG, `Final formatted data for ${formId}: isPlaying=${formattedData.isPlaying}, hasNext=${formattedData.hasNext}, hasPrevious=${formattedData.hasPrevious}, title=${formattedData.songTitle}, coverImage=${formattedData.coverImage ? 'present' : 'empty'}, showCover=${formattedData.showCover}`);
  145. // 创建卡片绑定数据
  146. const formData = formBindingData.createFormBindingData(formattedData);
  147. // 更新卡片
  148. await formProvider.updateForm(formId, formData);
  149. hilog.info(0x0000, TAG, `Widget updated successfully: ${formId}`);
  150. } catch (error) {
  151. hilog.error(0x0000, TAG, `Failed to update widget: ${error}`);
  152. }
  153. }
  154. /**
  155. * 批量更新所有卡片
  156. */
  157. async updateAllWidgets(data?: WidgetData): Promise<void> {
  158. try {
  159. if (!this.preferencesStore) {
  160. await this.initPreferences();
  161. }
  162. // 获取所有卡片ID
  163. const allKeys = await this.preferencesStore?.getAll();
  164. const emptyPrefs: PreferencesData = {};
  165. const widgetKeys = Object.keys(allKeys || emptyPrefs).filter(key => key.startsWith('widget_'));
  166. for (const key of widgetKeys) {
  167. const formId: string = key.replace('widget_', '');
  168. const widgetData: WidgetData = data || await this.getWidgetData(formId);
  169. await this.updateWidget(formId, widgetData);
  170. }
  171. hilog.info(0x0000, TAG, `Updated ${widgetKeys.length} widgets`);
  172. } catch (error) {
  173. hilog.error(0x0000, TAG, `Failed to update all widgets: ${error}`);
  174. }
  175. }
  176. /**
  177. * 删除卡片数据
  178. */
  179. async removeWidgetData(formId: string): Promise<void> {
  180. try {
  181. if (!this.preferencesStore) {
  182. await this.initPreferences();
  183. }
  184. const dataKey = `widget_${formId}`;
  185. await this.preferencesStore?.delete(dataKey);
  186. await this.preferencesStore?.flush();
  187. hilog.info(0x0000, TAG, `Widget data removed for form: ${formId}`);
  188. } catch (error) {
  189. hilog.error(0x0000, TAG, `Failed to remove widget data: ${error}`);
  190. }
  191. }
  192. /**
  193. * 格式化数据用于卡片显示
  194. */
  195. public formatDataForWidget(data: WidgetData): FormattedWidgetData {
  196. hilog.info(0x0000, TAG, `Formatting data for widget: hasNext=${data.playlist.hasNext}, hasPrevious=${data.playlist.hasPrevious}, currentIndex=${data.playlist.currentIndex}, totalCount=${data.playlist.totalCount}`);
  197. // 修复按钮状态:基于当前索引和总数重新计算正确的按钮状态
  198. let hasNext = data.playlist.hasNext;
  199. let hasPrevious = data.playlist.hasPrevious;
  200. hilog.info(0x0000, TAG, `Original button states: hasNext=${hasNext}, hasPrevious=${hasPrevious}, currentIndex=${data.playlist.currentIndex}, totalCount=${data.playlist.totalCount}`);
  201. // 如果有播放列表,重新计算按钮状态
  202. if (data.playlist.totalCount > 1) {
  203. const correctHasNext = data.playlist.currentIndex < data.playlist.totalCount - 1;
  204. const correctHasPrevious = data.playlist.currentIndex > 0;
  205. // 如果计算出的状态与当前状态不一致,进行修复
  206. if (hasNext !== correctHasNext || hasPrevious !== correctHasPrevious) {
  207. hasNext = correctHasNext;
  208. hasPrevious = correctHasPrevious;
  209. hilog.info(0x0000, TAG, `Fixed button states: hasNext=${hasNext}, hasPrevious=${hasPrevious}, currentIndex=${data.playlist.currentIndex}, totalCount=${data.playlist.totalCount}`);
  210. } else {
  211. hilog.info(0x0000, TAG, `Button states are correct: hasNext=${hasNext}, hasPrevious=${hasPrevious}`);
  212. }
  213. } else if (data.playlist.totalCount <= 1) {
  214. // 如果只有一首歌或没有歌,按钮都应该禁用
  215. hasNext = false;
  216. hasPrevious = false;
  217. hilog.info(0x0000, TAG, `Single or no song, buttons disabled: totalCount=${data.playlist.totalCount}`);
  218. }
  219. const formattedData: FormattedWidgetData = {
  220. // 播放状态
  221. isPlaying: data.playState.isPlaying,
  222. isPaused: data.playState.isPaused,
  223. isLoading: data.playState.isLoading,
  224. // 歌曲信息
  225. songTitle: this.truncateText(data.currentSong.title, 20),
  226. songArtist: this.truncateText(data.currentSong.artist, 15),
  227. songAlbum: this.truncateText(data.currentSong.album, 15),
  228. coverImage: data.currentSong.coverImagePath && data.currentSong.coverImagePath.trim() !== '' ? data.currentSong.coverImagePath : '',
  229. // 播放进度
  230. currentTime: data.progress.currentTimeText,
  231. totalTime: data.progress.totalTimeText,
  232. progressPercentage: data.progress.percentage,
  233. // 控制按钮状态(使用修复后的值)
  234. hasNext: hasNext,
  235. hasPrevious: hasPrevious,
  236. // 卡片配置
  237. showProgress: data.config.showProgress,
  238. showCover: data.config.showCover,
  239. widgetSize: data.config.size as string,
  240. // 时间戳用于强制更新
  241. timestamp: Date.now()
  242. };
  243. hilog.info(0x0000, TAG, `Formatted widget data: isPlaying=${formattedData.isPlaying}, hasNext=${formattedData.hasNext}, hasPrevious=${formattedData.hasPrevious}, title=${formattedData.songTitle}`);
  244. return formattedData;
  245. }
  246. /**
  247. * 截断文本
  248. */
  249. private truncateText(text: string, maxLength: number): string {
  250. if (text.length <= maxLength) {
  251. return text;
  252. }
  253. return text.substring(0, maxLength - 1) + '…';
  254. }
  255. /**
  256. * 启动缓存清理定时器
  257. */
  258. private startCacheCleanup(): void {
  259. setInterval(() => {
  260. this.cleanExpiredCache();
  261. }, 60 * 1000); // 每分钟清理一次过期缓存
  262. }
  263. /**
  264. * 清理过期缓存
  265. */
  266. private cleanExpiredCache(): void {
  267. const now = Date.now();
  268. const expiredKeys: string[] = [];
  269. this.dataCache.forEach((item: CacheItem, key: string) => {
  270. if (now > item.expiry) {
  271. expiredKeys.push(key);
  272. }
  273. });
  274. expiredKeys.forEach((key: string) => {
  275. this.dataCache.delete(key);
  276. });
  277. if (expiredKeys.length > 0) {
  278. hilog.info(0x0000, TAG, `Cleaned ${expiredKeys.length} expired cache items`);
  279. }
  280. }
  281. /**
  282. * 从缓存获取数据
  283. */
  284. private getCachedData(formId: string): WidgetData | null {
  285. const cacheKey = `cache_${formId}`;
  286. const cacheItem = this.dataCache.get(cacheKey);
  287. if (cacheItem && Date.now() < cacheItem.expiry) {
  288. hilog.info(0x0000, TAG, `Cache hit for form: ${formId}`);
  289. return cacheItem.data;
  290. }
  291. if (cacheItem) {
  292. // 缓存已过期,删除
  293. this.dataCache.delete(cacheKey);
  294. hilog.info(0x0000, TAG, `Cache expired for form: ${formId}`);
  295. }
  296. return null;
  297. }
  298. /**
  299. * 设置缓存数据
  300. */
  301. private setCachedData(formId: string, data: WidgetData): void {
  302. const cacheKey = `cache_${formId}`;
  303. const now = Date.now();
  304. const cacheItem: CacheItem = {
  305. data: data,
  306. timestamp: now,
  307. expiry: now + CACHE_EXPIRY_TIME
  308. };
  309. this.dataCache.set(cacheKey, cacheItem);
  310. hilog.info(0x0000, TAG, `Data cached for form: ${formId}`);
  311. }
  312. /**
  313. * 获取卡片数据(带缓存)
  314. */
  315. async getWidgetDataWithCache(formId: string): Promise<WidgetData> {
  316. // 先尝试从缓存获取
  317. const cachedData = this.getCachedData(formId);
  318. if (cachedData) {
  319. return cachedData;
  320. }
  321. // 缓存未命中,从持久化存储获取
  322. const data = await this.getWidgetData(formId);
  323. // 设置缓存
  324. this.setCachedData(formId, data);
  325. return data;
  326. }
  327. /**
  328. * 更新卡片数据(带缓存)
  329. */
  330. async updateWidgetWithCache(formId: string, data: WidgetData): Promise<void> {
  331. // 更新缓存
  332. this.setCachedData(formId, data);
  333. // 更新卡片显示
  334. await this.updateWidget(formId, data);
  335. }
  336. /**
  337. * 清除指定卡片的缓存
  338. */
  339. clearWidgetCache(formId: string): void {
  340. const cacheKey = `cache_${formId}`;
  341. if (this.dataCache.has(cacheKey)) {
  342. this.dataCache.delete(cacheKey);
  343. hilog.info(0x0000, TAG, `Cache cleared for form: ${formId}`);
  344. }
  345. }
  346. /**
  347. * 清除所有缓存
  348. */
  349. clearAllCache(): void {
  350. const cacheSize = this.dataCache.size;
  351. this.dataCache.clear();
  352. hilog.info(0x0000, TAG, `All cache cleared, ${cacheSize} items removed`);
  353. }
  354. /**
  355. * 获取缓存统计信息
  356. */
  357. getCacheStats(): CacheStats {
  358. const stats: CacheStats = {
  359. size: this.dataCache.size,
  360. hitRate: 0, // 可以在实际使用中统计命中率
  361. lastUpdate: this.lastUpdateTime
  362. };
  363. return stats;
  364. }
  365. /**
  366. * 预热缓存
  367. */
  368. async preloadCache(): Promise<void> {
  369. try {
  370. if (!this.preferencesStore) {
  371. await this.initPreferences();
  372. }
  373. const allKeys = await this.preferencesStore?.getAll();
  374. const emptyPrefs: PreferencesData = {};
  375. const widgetKeys = Object.keys(allKeys || emptyPrefs).filter(key => key.startsWith('widget_'));
  376. for (const key of widgetKeys) {
  377. const formId: string = key.replace('widget_', '');
  378. const data: WidgetData = await this.getWidgetData(formId);
  379. this.setCachedData(formId, data);
  380. }
  381. hilog.info(0x0000, TAG, `Cache preloaded for ${widgetKeys.length} widgets`);
  382. } catch (error) {
  383. hilog.error(0x0000, TAG, `Failed to preload cache: ${error}`);
  384. }
  385. }
  386. /**
  387. * 检查是否为格式化的卡片数据
  388. */
  389. private isFormattedWidgetData(data: WidgetData | FormattedWidgetData): boolean {
  390. // FormattedWidgetData有timestamp字段,而WidgetData没有
  391. return (data as FormattedWidgetData).timestamp !== undefined && typeof (data as FormattedWidgetData).timestamp === 'number';
  392. }
  393. /**
  394. * 将格式化数据转换为WidgetData
  395. */
  396. private convertToWidgetData(formattedData: FormattedWidgetData): WidgetData {
  397. const widgetData: WidgetData = {
  398. playState: {
  399. isPlaying: formattedData.isPlaying,
  400. isPaused: formattedData.isPaused,
  401. isLoading: formattedData.isLoading
  402. },
  403. currentSong: {
  404. id: '', // FormattedWidgetData中没有id,使用空字符串
  405. title: formattedData.songTitle,
  406. artist: formattedData.songArtist,
  407. album: formattedData.songAlbum,
  408. coverImagePath: formattedData.coverImage,
  409. duration: 0 // FormattedWidgetData中没有duration,使用0
  410. },
  411. progress: {
  412. currentPosition: 0, // 需要从时间文本反推,这里简化处理
  413. duration: 0,
  414. percentage: formattedData.progressPercentage,
  415. currentTimeText: formattedData.currentTime,
  416. totalTimeText: formattedData.totalTime
  417. },
  418. playlist: {
  419. hasNext: formattedData.hasNext,
  420. hasPrevious: formattedData.hasPrevious,
  421. currentIndex: 0,
  422. totalCount: 0
  423. },
  424. config: {
  425. size: this.parseWidgetSize(formattedData.widgetSize),
  426. theme: WidgetTheme.AUTO,
  427. showProgress: formattedData.showProgress,
  428. showCover: formattedData.showCover
  429. }
  430. };
  431. return widgetData;
  432. }
  433. /**
  434. * 解析卡片尺寸字符串
  435. */
  436. private parseWidgetSize(sizeStr: string): WidgetSize {
  437. switch (sizeStr.toLowerCase()) {
  438. case 'small':
  439. return WidgetSize.SMALL;
  440. case 'large':
  441. return WidgetSize.LARGE;
  442. case 'medium':
  443. default:
  444. return WidgetSize.MEDIUM;
  445. }
  446. }
  447. }