• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2022-2023 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import prompt from '@ohos.prompt';
17import CommonEvent from '@ohos.commonEvent';
18import {
19  AppItemInfo,
20  CheckEmptyUtils,
21  EventConstants,
22  CommonConstants,
23  GridLayoutItemInfo,
24  FormManager,
25  FormModel,
26  Logger,
27  LauncherAbilityManager,
28  MenuInfo,
29  RdbManager,
30  ResourceManager,
31  FormCardItem
32} from '@ohos/base';
33import formHost from '@ohos.app.form.formHost';
34import { BusinessError } from '@ohos.base';
35
36const TAG: string = 'LayoutInfoModel';
37
38export const SHOPPING_BUNDLE: string = 'com.samples.asorangeshopping';
39
40const SYSTEM_APPLICATIONS: string = 'com.ohos.adminprovisioning,com.ohos.launcher,ohos.samples.launcher,com.ohos.systemui,com.ohos.devicemanagerui,com.ohos.callui,com.example.kikakeyboard,com.ohos.contactdataability,com.ohos.telephonydataability,com.ohos.medialibrary.MediaLibraryDataA,com.ohos.medialibrary.MediaScannerAbilityA'
41const KEY_NAME = 'name';
42
43export class DesktopLayoutModel {
44  public static layoutInfoModel: DesktopLayoutModel | undefined = undefined;
45  private layoutInfo: Array<Array<GridLayoutItemInfo>> = [];
46  private readonly mSystemApplicationName = SYSTEM_APPLICATIONS.split(',');
47  private mLauncherAbilityManager: LauncherAbilityManager | undefined = undefined;
48  private context: Context;
49
50  constructor(context: Context) {
51    this.context = context
52    this.mLauncherAbilityManager = LauncherAbilityManager.getInstance(context);
53    this.mLauncherAbilityManager.registerLauncherAbilityChangeListener(this.appChangeListener);
54  }
55
56  appChangeListener = (event: string, bundleName: string, userId: string) => {
57    Logger.info(TAG, `appChangeListener event = ${event},bundle = ${bundleName}`);
58    FormModel.updateAppItemFormInfo(bundleName);
59    if (event === EventConstants.EVENT_PACKAGE_REMOVED) {
60      this.removeItemByBundle(bundleName);
61    } else if (event === EventConstants.EVENT_PACKAGE_ADDED) {
62      this.mLauncherAbilityManager?.getAppInfoByBundleName(bundleName).then(appInfo => {
63        Logger.debug(TAG, `appChangeListener EVENT_PACKAGE_ADDED,info = ${JSON.stringify(appInfo)}`);
64        this.addAppToDesktop(appInfo, true);
65      })
66    }
67  }
68
69
70  /**
71   * Get the application data model object.
72   *
73   * @return {object} application data model singleton
74   */
75  public static getInstance(context: Context): DesktopLayoutModel {
76    if (DesktopLayoutModel.layoutInfoModel == null || DesktopLayoutModel.layoutInfoModel === undefined) {
77      DesktopLayoutModel.layoutInfoModel = new DesktopLayoutModel(context);
78    }
79    return DesktopLayoutModel.layoutInfoModel;
80  }
81
82  private async removeItemByBundle(bundleName: string): Promise<void> {
83    let page = this.layoutInfo.length;
84    for (let i = 0;i < page; i++) {
85      for (let j = 0;j < this.layoutInfo[i].length; j++) {
86        if (this.layoutInfo[i][j].bundleName === bundleName) {
87          await this.removeItemFromDeskTop(this.layoutInfo[i][j]);
88        }
89      }
90    }
91  }
92
93  /**
94   * getAppItemFormInfo
95   *
96   * @param bundleName
97   */
98  getAppItemFormInfo(bundleName: string) {
99    return FormModel.getAppItemFormInfo(bundleName);
100  }
101
102  /**
103   * buildMenuInfoList
104   *
105   * @param appInfo: GridLayoutItemInfo
106   */
107  buildMenuInfoList(appInfo: GridLayoutItemInfo, dialog: CustomDialogController) {
108    if (CheckEmptyUtils.isEmpty(appInfo)) {
109      return undefined;
110    }
111    let menuInfoList = new Array<MenuInfo>();
112    let open = new MenuInfo();
113    open.menuImgSrc = $r('app.media.ic_public_add_norm');
114    open.menuText = $r('app.string.app_menu_open');
115    open.onMenuClick = () => {
116      this.jumpTo(appInfo.abilityName, appInfo.bundleName);
117    }
118    menuInfoList.push(open);
119
120    Logger.info(TAG, `buildMenuInfoList getAppItemFormInfo,bundleName =  ${appInfo.bundleName}`);
121    const formInfoList = FormModel.getAppItemFormInfo(appInfo.bundleName);
122    Logger.info(TAG, `buildMenuInfoList formInfoList = ${JSON.stringify(formInfoList)}`);
123    if (!CheckEmptyUtils.isEmptyArr(formInfoList)) {
124      let addFormToDeskTopMenu = new MenuInfo();
125      addFormToDeskTopMenu.menuImgSrc = $r('app.media.ic_public_app');
126      addFormToDeskTopMenu.menuText = $r('app.string.add_form_to_desktop');
127      addFormToDeskTopMenu.onMenuClick = () => {
128        Logger.info(TAG, 'Launcher click menu item into add form to desktop view');
129        if (!CheckEmptyUtils.isEmpty(appInfo)) {
130          AppStorage.SetOrCreate('formAppInfo', appInfo);
131          Logger.info(TAG, 'Launcher AppStorage.SetOrCreate formAppInfo');
132          this.jumpToFormManagerView();
133        }
134      }
135      menuInfoList.push(addFormToDeskTopMenu);
136    }
137
138    const uninstallMenu = new MenuInfo();
139    uninstallMenu.menuImgSrc = $r('app.media.ic_public_delete');
140    uninstallMenu.menuText = $r('app.string.uninstall');
141    uninstallMenu.onMenuClick = () => {
142      Logger.info(TAG, 'Launcher click menu item uninstall');
143      if (!CheckEmptyUtils.isEmpty(dialog)) {
144        dialog.open();
145      }
146    }
147    menuInfoList.push(uninstallMenu);
148    return menuInfoList;
149  }
150
151  /**
152   * buildCardInfoList
153   *
154   * @param dialog
155   */
156  buildCardInfoList(dialog: CustomDialogController) {
157    let menuInfoList = new Array<MenuInfo>();
158    const uninstallMenu = new MenuInfo();
159    uninstallMenu.menuImgSrc = $r('app.media.ic_public_delete');
160    uninstallMenu.menuText = $r('app.string.remove');
161    uninstallMenu.onMenuClick = () => {
162      Logger.info(TAG, 'Launcher click menu item uninstall');
163      if (!CheckEmptyUtils.isEmpty(dialog)) {
164        dialog.open();
165      }
166    }
167    menuInfoList.push(uninstallMenu);
168    return menuInfoList;
169  }
170
171  /**
172   * getAppName
173   *
174   * @param cacheKey
175   */
176  getAppName(cacheKey: string): string {
177    return ResourceManager.getInstance(this.context).getAppResourceCache(cacheKey, KEY_NAME);
178  }
179
180  /**
181   * jump to form manager
182   * @param formInfo
183
184   * */
185  jumpToFormManagerView(): void {
186    CommonEvent.publish(EventConstants.EVENT_ENTER_FORM_MANAGER, () => {
187      Logger.info(TAG, 'publish EVENT_ENTER_FORM_MANAGER');
188    });
189  }
190
191  /**
192   * Start target ability
193   *
194   * @param bundleName target bundle name
195   * @param abilityName target ability name
196   */
197  jumpTo(abilityName: string | undefined, bundleName: string | undefined): void {
198    this.mLauncherAbilityManager?.startLauncherAbilityFromRecent(abilityName, bundleName);
199  }
200
201  /**
202   * getLayoutInfoCache
203   */
204  getLayoutInfoCache() {
205    return this.layoutInfo;
206  }
207
208  /**
209   * Get the list of apps displayed on the desktop (private function).
210   *
211   * @return {array} bundleInfoList, excluding system applications
212   */
213  async getAppListAsync(): Promise<AppItemInfo[]> {
214    let allAbilityList: AppItemInfo[] | undefined = await this.mLauncherAbilityManager?.getLauncherAbilityList();
215    Logger.info(TAG, `getAppListAsync allAbilityList length: ${allAbilityList?.length}`);
216    let launcherAbilityList: AppItemInfo[] = [];
217    for (let i = 0; i < allAbilityList!.length; i++) {
218      if (this.mSystemApplicationName.indexOf(allAbilityList![i].bundleName) === CommonConstants.INVALID_VALUE) {
219        launcherAbilityList.push(allAbilityList![i]);
220        FormModel.updateAppItemFormInfo(allAbilityList![i].bundleName);
221      }
222    }
223    Logger.debug(TAG, `getAppListAsync launcherAbiltyList length: ${launcherAbilityList.length}`);
224    return launcherAbilityList;
225  }
226
227  /**
228   * getLayoutInfo
229   */
230  async getLayoutInfo() {
231    await RdbManager.initRdbConfig(this.context);
232    let infos = await this.getAppListAsync();
233    let gridLayoutItemInfos = await RdbManager.queryLayoutInfo();
234    Logger.info(TAG, `queryLayoutInfo,gridLayoutItemInfos = ${gridLayoutItemInfos.length}`);
235    let result: Array<Array<GridLayoutItemInfo>> = [];
236    let plusApps: Array<AppItemInfo> = [];
237    // 如果查询到的数据长度是0,说明之前没有过数据,此时初始化数据并插入
238    if (gridLayoutItemInfos.length === 0) {
239      let result = this.initPositionInfos(infos);
240      await RdbManager.insertData(result);
241      this.layoutInfo = result;
242      Logger.info(TAG, `getLayoutInfo result0,${JSON.stringify(this.layoutInfo)}`);
243      this.addEmptyCard();
244      return result;
245    }
246    // 数据库中查询到了数据,则优先加载数据库中的应用和卡片,剩余的应用图标在最后一个图标的位置后面添加
247    else {
248      for (let i = 0;i < infos.length; i++) {
249        let find = false;
250        for (let j = 0;j < gridLayoutItemInfos.length; j++) {
251          if (infos[i].bundleName === gridLayoutItemInfos[j].bundleName) {
252            if (gridLayoutItemInfos[j].page >= result.length) {
253              result.push([]);
254            }
255            result[gridLayoutItemInfos[j].page].push(gridLayoutItemInfos[j]);
256            find = true;
257          }
258        }
259        if (!find) {
260          plusApps.push(infos[i]);
261        }
262      }
263      this.layoutInfo = result
264      Logger.info(TAG, `getLayoutInfo result1,${JSON.stringify(this.layoutInfo[0].length)}`);
265      // 加载完数据库中的后,剩余的app
266      if (plusApps.length > 0) {
267        Logger.info(TAG, `加载完数据库中的后,剩余的app`)
268        for (let k = 0;k < plusApps.length; k++) {
269          let item = plusApps[k];
270          if (item) {
271            this.addAppToDesktop(item, false);
272          }
273        }
274      }
275      Logger.info(TAG, `getLayoutInfo result2,${JSON.stringify(result[0].length)}`);
276      this.addEmptyCard();
277      return this.layoutInfo;
278    }
279  }
280
281  // mock一下桌面静态图的信息
282  private mockItem(): GridLayoutItemInfo {
283    let mockTemp = new GridLayoutItemInfo();
284    mockTemp.typeId = CommonConstants.TYPE_IMAGE;
285    mockTemp.page = this.layoutInfo.length;
286    mockTemp.bundleName = SHOPPING_BUNDLE + '1';
287    mockTemp.row = 0;
288    mockTemp.column = 0;
289    mockTemp.area = CommonConstants.DEFAULT_IMAGE_AREA;
290    return mockTemp;
291  }
292
293  // 检测免安装的应用是否已经替换过占位卡片,没有时添加一个空的占位卡片
294  private addEmptyCard(): void {
295    Logger.info(TAG, 'addEmptyCard');
296    for (let i = 0; i < this.layoutInfo.length; i++) {
297      for (let j = 0; j < this.layoutInfo[i].length; j++) {
298        if (this.layoutInfo[i][j].bundleName === SHOPPING_BUNDLE) {
299          return;
300        }
301      }
302    }
303    this.layoutInfo.push([this.mockItem()]);
304  }
305
306  private async addAppToDesktop(appInfo: AppItemInfo | undefined, isRefresh: boolean) {
307    if (CheckEmptyUtils.isEmpty(appInfo)) {
308      return;
309    }
310    let pageInfos = this.layoutInfo;
311    for (let i = 0;i < pageInfos.length; i++) {
312      Logger.info(TAG, `removeCardFromDeskTop pageInfos${i}`);
313      for (let j = 0;j < pageInfos[i].length; j++) {
314        if (pageInfos[i][j].bundleName === appInfo?.bundleName && pageInfos[i][j].abilityName === appInfo?.abilityName) {
315          return;
316        }
317      }
318    }
319    let gridItem: GridLayoutItemInfo | undefined = this.covertAppItemToGridItem(appInfo, 0, 0, 0);
320    let page = this.layoutInfo.length;
321    gridItem = this.updateItemLayoutInfo(gridItem);
322    if (gridItem.page >= page) {
323      this.layoutInfo.push([]);
324    }
325    this.layoutInfo[gridItem.page].push(gridItem);
326    Logger.info(TAG, `addAppToDesktop item ${JSON.stringify(gridItem)}`);
327    await RdbManager.initRdbConfig(this.context);
328    await RdbManager.insertItem(gridItem);
329    if (isRefresh) {
330      AppStorage.SetOrCreate('isRefresh', true);
331    }
332  }
333
334  /**
335   * uninstallAppItem
336   *
337   * @param itemInfo: GridLayoutItemInfo
338   */
339  async uninstallAppItem(itemInfo: GridLayoutItemInfo) {
340    if (CheckEmptyUtils.isEmpty(itemInfo)) {
341      return;
342    }
343    let appInfo = await this.mLauncherAbilityManager?.getAppInfoByBundleName(itemInfo.bundleName);
344    if (CheckEmptyUtils.isEmpty(appInfo)) {
345      return;
346    }
347    if (appInfo?.isUninstallAble) {
348      this.mLauncherAbilityManager?.uninstallLauncherAbility(itemInfo.bundleName, (err: BusinessError) => {
349        if (err.code == CommonConstants.UNINSTALL_SUCCESS) {
350        }
351        this.informUninstallResult(err.code);
352      })
353    } else {
354      this.informUninstallResult(CommonConstants.UNINSTALL_FORBID);
355    }
356  }
357
358  private informUninstallResult(resultCode: number) {
359    let uninstallMessage: string = '';
360    if (resultCode === CommonConstants.UNINSTALL_FORBID) {
361      uninstallMessage = this.context?.resourceManager.getStringSync($r('app.string.disable_uninstall').id);
362    } else if (resultCode === CommonConstants.UNINSTALL_SUCCESS) {
363      uninstallMessage = this.context.resourceManager.getStringSync($r('app.string.uninstall_success').id);
364    } else {
365      uninstallMessage = this.context.resourceManager.getStringSync($r('app.string.uninstall_failed').id);
366    }
367    prompt.showToast({
368      message: uninstallMessage
369    })
370  }
371
372  /**
373   * initPositionInfos
374   *
375   * @param appInfos
376   */
377  initPositionInfos(appInfos: Array<AppItemInfo>) {
378    if (CheckEmptyUtils.isEmptyArr(appInfos)) {
379      return [];
380    }
381    Logger.info(TAG, `initPositionInfos, appInfos size = ${appInfos.length}`);
382    let countsOnePage = CommonConstants.DEFAULT_COLUMN_COUNT * CommonConstants.DEFAULT_ROW_COUNT;
383    let result: Array<Array<GridLayoutItemInfo>> = [];
384    let page = Math.floor(appInfos.length / countsOnePage) + 1;
385    for (let i = 0;i < page; i++) {
386      let item: Array<GridLayoutItemInfo> | undefined = [];
387      result.push(item);
388    }
389    Logger.info(TAG, `initPositionInfos result0 = ${JSON.stringify(result)}`);
390    for (let j = 0;j < appInfos.length; j++) {
391      let item = appInfos[j];
392      Logger.info(TAG, `initPositionInfos infos[${j}], item = ${JSON.stringify(item)}`);
393      // 获取appLabelId,之后需要修改包名
394      if (appInfos[j].bundleName === SHOPPING_BUNDLE) {
395        AppStorage.SetOrCreate('cardLabelId', appInfos[j].appLabelId);
396      }
397      let page = Math.floor(j / countsOnePage);
398      let column = Math.floor(j % CommonConstants.DEFAULT_COLUMN_COUNT);
399      let row = Math.floor(j / CommonConstants.DEFAULT_COLUMN_COUNT) % countsOnePage;
400      let gridItem: GridLayoutItemInfo | undefined = this.covertAppItemToGridItem(item, page, column, row);
401      if (!CheckEmptyUtils.isEmpty(gridItem)) {
402        result[page].push(gridItem!);
403      }
404      Logger.info(TAG, `initPositionInfos infos[${j}], page = ${page},row = ${row},column = ${column}`);
405    }
406    Logger.info(TAG, `initPositionInfos result1 = ${JSON.stringify(result)}`);
407    return result;
408  }
409
410  private covertAppItemToGridItem(item: AppItemInfo | undefined, page: number, column: number, row: number) {
411    if (CheckEmptyUtils.isEmpty(item)) {
412      return undefined;
413    }
414    let gridItem: GridLayoutItemInfo = new GridLayoutItemInfo();
415    gridItem.appName = item?.appName;
416    gridItem.appIconId = item?.appIconId;
417    gridItem.bundleName = item?.bundleName;
418    gridItem.moduleName = item?.moduleName;
419    gridItem.abilityName = item?.abilityName;
420    gridItem.container = -100;
421    gridItem.page = page;
422    gridItem.column = column;
423    gridItem.row = row;
424    gridItem.area = [1, 1];
425    gridItem.typeId = 0;
426    return gridItem;
427  }
428
429  /**
430   * createCardToDeskTop
431   *
432   * @param formCardItem
433   */
434  async createCardToDeskTop(formCardItem: FormCardItem) {
435    if (CheckEmptyUtils.isEmpty(formCardItem)) {
436      return;
437    }
438    Logger.info(TAG, `createCardToDeskTop formCardItem ${JSON.stringify(formCardItem)}`);
439    let gridItem = this.createNewCardItemInfo(formCardItem);
440    if (formCardItem.bundleName === SHOPPING_BUNDLE) {
441      gridItem!.page = this.layoutInfo.length;
442      gridItem!.row = 0;
443      gridItem!.column = 0;
444    } else {
445      gridItem = this.updateItemLayoutInfo(gridItem);
446    }
447    if (gridItem!.page >= this.layoutInfo.length) {
448      this.layoutInfo.push([]);
449    }
450    this.layoutInfo[gridItem!.page].push(gridItem!);
451    await RdbManager.initRdbConfig(this.context);
452    await RdbManager.insertItem(gridItem);
453    Logger.info(TAG, `createCardToDeskTop gridItem =  ${JSON.stringify(gridItem)}`);
454    AppStorage.SetOrCreate('isRefresh', true);
455  }
456
457  /**
458   * remove item from desktop
459   *
460   * @param item
461   */
462  async removeItemFromDeskTop(item: GridLayoutItemInfo) {
463    if (CheckEmptyUtils.isEmpty(item)) {
464      return;
465    }
466    Logger.info(TAG, 'removeCardFromDeskTop start');
467    let pageInfos = this.layoutInfo;
468    searchCircle:for (let i = 0;i < pageInfos.length; i++) {
469      Logger.info(TAG, `removeCardFromDeskTop pageInfos${i}`);
470      for (let j = 0;j < pageInfos[i].length; j++) {
471        if (pageInfos[i][j].bundleName === item.bundleName && pageInfos[i][j].page === item.page
472        && pageInfos[i][j].row === item.row && pageInfos[i][j].column === item.column) {
473          Logger.debug(TAG, `removeCardFromDeskTop pageInfos${i}${j} is find,remove`);
474          pageInfos[i].splice(j, 1);
475          // 移除后是空白屏幕,移除屏幕
476          if (pageInfos[i].length === 0) {
477            pageInfos.splice(i, 1);
478          }
479          break searchCircle;
480        }
481      }
482    }
483    this.layoutInfo = pageInfos;
484    await RdbManager.deleteItemByPosition(item.page, item.row, item.column);
485    formHost.deleteForm(item.cardId.toString(), (err) => {
486      if (err) {
487        Logger.info(TAG, `deleteForm err: ${JSON.stringify(err)}`);
488      } else {
489        Logger.info(TAG, 'deleteForm success');
490      }
491    })
492    Logger.info(TAG, `removeCardFromDeskTop item= ${JSON.stringify(item)}`);
493    AppStorage.SetOrCreate('isRefresh', true);
494  }
495
496  private updateItemLayoutInfo(item: GridLayoutItemInfo | undefined): GridLayoutItemInfo {
497    Logger.info(TAG, 'updateItemLayoutInfo' + this.layoutInfo.length);
498    let page = this.layoutInfo.length;
499    const row = CommonConstants.DEFAULT_ROW_COUNT;
500    const column = CommonConstants.DEFAULT_COLUMN_COUNT;
501    let isNeedNewPage = true;
502    for (let i = 0; i < page; i++) {
503      for (let y = 0; y < row; y++) {
504        for (let x = 0; x < column; x++) {
505          Logger.info(TAG, `updateItemLayoutInfo page=${page}, startColumn=${x}, startRow=${y}`);
506          if (this.isPositionValid(item, i, x, y)) {
507            isNeedNewPage = false
508            item!.page = i
509            item!.column = x
510            item!.row = y
511            return item!;
512          }
513        }
514      }
515    }
516
517    if (isNeedNewPage) {
518      item!.page = page
519      item!.column = 0
520      item!.row = 0
521    }
522    return item!;
523  }
524
525  private isPositionValid(item: GridLayoutItemInfo | undefined, page: number, startColumn: number, startRow: number) {
526    const row = CommonConstants.DEFAULT_ROW_COUNT;
527    const column = CommonConstants.DEFAULT_COLUMN_COUNT;
528    if ((startRow + item!.area[0]) > row || (startColumn + item!.area[1]) > column) {
529      Logger.info(TAG, 'isPositionValid return false 1');
530      return false;
531    }
532    let isValid = true;
533    for (let x = startColumn; x < startColumn + item!.area[1]; x++) {
534      for (let y = startRow; y < startRow + item!.area[0]; y++) {
535        if (this.isPositionOccupied(page, x, y)) {
536          Logger.info(TAG, `isPositionValid isPositionOccupied page=${page},x=${x},y=${y}`);
537          isValid = false;
538          break;
539        }
540      }
541    }
542    return isValid;
543  }
544
545  private isPositionOccupied(page: number, column: number, row: number) {
546    const layoutInfo = this.layoutInfo[page];
547    // current page has space
548    for (let item of layoutInfo) {
549      const xMatch = (column >= item.column) && (column < item.column + item.area[1]);
550      const yMatch = (row >= item.row) && (row < item.row + item.area[0]);
551      if (xMatch && yMatch) {
552        return true;
553      }
554    }
555    return false;
556  }
557
558  private createNewCardItemInfo(formCardItem: FormCardItem): GridLayoutItemInfo | undefined {
559    if (CheckEmptyUtils.isEmpty(formCardItem)) {
560      return undefined;
561    }
562    let gridItem: GridLayoutItemInfo = new GridLayoutItemInfo();
563    gridItem.appName = formCardItem.appName;
564    gridItem.typeId = CommonConstants.TYPE_CARD;
565    gridItem.cardId = formCardItem.cardId;
566    gridItem.cardName = formCardItem.cardName;
567    gridItem.bundleName = formCardItem.bundleName;
568    gridItem.moduleName = formCardItem.moduleName;
569    gridItem.abilityName = formCardItem.abilityName;
570    gridItem.container = -100;
571    gridItem.page = 0;
572    gridItem.column = 0;
573    gridItem.row = 0;
574    gridItem.area = FormManager.getCardSize(formCardItem.dimension);
575    return gridItem;
576  }
577}