1/*
2 * Copyright (c) 2021-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
16/**
17 * [系统全局globalThis挂载的对象类型定义]
18 */
19import window from '@ohos.window'
20import display from '@ohos.display'
21import deviceInfo from '@ohos.deviceInfo'
22import type context from '@ohos.app.ability.common'
23import i18n from '@ohos.i18n'
24import pointer from '@ohos.multimodalInput.pointer';
25import type { BusinessError } from '@ohos.base'
26
27import { Logger } from '../../util/HiLogger'
28import { appStorageKeys, setOrCreateAppStorage } from '../AppStorageHelper'
29import { createOrGet, globalKeys } from '../GlobalThisHelper'
30import { Util } from '../../util/Util'
31
32const logger: Logger = new Logger("GlobalModel")
33// PC的头部间距,16 / 160
34const PC_TOP_PADDING: number = 0.1
35// 分屏比例
36const SPLIT_RATIO_MINI = 5 / 12
37const SPLIT_RATIO_MAX = 7 / 12
38// 白色字符串
39const WHITE: string = "#ffffffff"
40// 黑色字符串
41const BLACK: string = "#ff000000"
42// 透明
43const TRANS: string = "#00ffffff"
44
45/**
46 * 窗口对象
47 */
48export class AppScreen {
49  // abilityContext
50  context: context.UIAbilityContext
51  // stage模型的窗口对象
52  windowStage: window.WindowStage
53  // 屏幕宽度
54  width: number = 0
55  // 屏幕高度
56  height: number = 0
57  // 窗口x坐标
58  windowPosX: number = 0
59  // 窗口y坐标
60  windowPosY: number = 0
61  // 窗口宽度
62  windowWidth: number = 0
63  // 窗口高度
64  windowHeight: number = 0
65  // 状态栏高度
66  topHeight: number = 0
67  // 导航栏高度
68  bottomHeight: number = 0
69  // 屏幕密度类型,用来计算lpx真实物理大小
70  densityType: DensityTypes = DensityTypes.SM
71  // 窗口配置变化监听
72  configChangeListener: Array<(densityType: DensityTypes) => void> = []
73  // 接收窗口变化后一系列配置完成后的promise
74  lastWindowSizeResult: Promise<boolean> = Promise.resolve(true)
75  // 横竖屏模式设定
76  orientationSetting: window.Orientation = window.Orientation?.AUTO_ROTATION_LANDSCAPE_RESTRICTED
77  // 当前是横屏1还是竖屏0
78  orientation: number = 0
79  // 是否已经设置过沉浸式
80  isImmersiveSet: boolean = false
81  // 应用启动后是否设置过densityType
82  isDensityTypeSet: boolean = false
83  // DPI mate40pro 560
84  densityDPI: number = 0
85  // 是否全屏
86  isFullScreen: boolean = false
87  // 沉浸式,可以显示状态栏文字
88  isLayoutFullScreen: boolean = false
89  // 沉浸式,不显示状态栏文字
90  isFullScreenWindow: boolean = false
91  // 天枢wgr是否分屏, 高度为设备高度,且宽度小于设备宽度
92  isPCMultiWindow: boolean = false
93  // 状态栏背景是否是深色
94  isTopBgDark: boolean = false
95  // 状态栏背景颜色
96  statusBarBgColor: string = ""
97  // 导航栏背景是否是深色
98  isBottomBgDark: boolean = false
99  // 屏幕是否常亮
100  isKeepScreenOn: boolean = false
101  // 判断当前设备是否可折叠
102  isFoldScreen: boolean = false
103  // 获取当前屏幕折叠状态  展开态: 1 折叠态: 2 半折态: 3
104  displayStatus: number = 1
105  // 获取当前屏幕显示模式0:折叠状态未知1:折叠状态为完全展开2:折叠状态为折叠3:折叠状态为半折叠。半折叠指完全展开和折叠之间的状态。
106  displayMode: number = 0
107  // 分屏比例
108  appSplitRatio: AppSplitRatios = AppSplitRatios.NO
109  // 当前窗口模式
110  windowStatusType: window.WindowStatusType = window.WindowStatusType.UNDEFINED
111  // 应用信息
112  app: MusicApp
113
114  // 构造函数
115  constructor(context: context.UIAbilityContext, windowStage: window.WindowStage) {
116    logger.info('new Screen')
117    this.context = context
118    this.windowStage = windowStage
119    this.app = createOrGet(MusicApp, globalKeys.app)
120    this.init()
121  }
122
123  /**
124   * 初始化
125   */
126  async init(): Promise<void> {
127    this.enterImmersion()
128    this.getDisplayStatus()
129    this.bindWindowSizeListener()
130    this.bindFoldStatusChange()
131    this.bindFoldDisplayModeChange()
132    this.setOrientationSetting()
133    this.lastWindowSizeResult = this.windowResize()
134    this.bindWindowEventListener()
135  }
136
137  /**
138   * 监听应用窗口焦点变化
139   */
140  bindWindowEventListener(): void {
141    window.getLastWindow(this.context).then((windowClass) => {
142      logger.info('Succeeded in obtaining the top window. Data: ' + windowClass);
143      if (!windowClass) {
144        return
145      }
146      try {
147        windowClass.on('windowEvent', (value) => {
148          if (value === window.WindowEventType.WINDOW_ACTIVE) {
149            logger.info('Window event happened. Event:' + value);
150            this.app?.setActiveNo()
151          }
152        });
153      } catch (err) {
154        logger.error('Failed to register callback. Cause: ' + JSON.stringify(err));
155      }
156    }).catch((err: BusinessError) => {
157      logger.error('Failed to obtain the top window. Cause: ' + JSON.stringify(err));
158    });
159  }
160
161  getDisplayStatus(): void {
162    this.isFoldScreen = display.isFoldable();
163
164    this.displayStatus = display.getFoldStatus();
165
166    this.displayMode = display.getFoldDisplayMode();
167  }
168
169  setOrientationSetting(): void {
170    let orientation: window.Orientation = window.Orientation.AUTO_ROTATION_RESTRICTED
171    let globalDeviceInfo: DeviceInfo = createOrGet(DeviceInfo, globalKeys.deviceInfo)
172    let isPhone: boolean = globalDeviceInfo.deviceType === DeviceTypes.PHONE
173    logger.info('this.isFoldScreen' + this.isFoldScreen + 'this.displayMode' + this.displayMode)
174    if (this.isFoldScreen) {
175      if (this.displayMode === 1) {
176        logger.info('this.isFoldScreen' + this.isFoldScreen + 'this.displayMode' + this.displayMode)
177        orientation = window.Orientation.AUTO_ROTATION_RESTRICTED
178      } else if (this.displayMode === 2) {
179        logger.info('this.isFoldScreen' + this.isFoldScreen + 'this.displayMode' + this.displayMode)
180        orientation = window.Orientation.PORTRAIT
181      } else if (this.displayMode === 3 || this.displayMode === 0) {
182        return
183      }
184    } else if (isPhone) {
185      // 手机横屏MD修改判断设备类型
186      orientation = window.Orientation.PORTRAIT
187    } else {
188      orientation = window.Orientation.AUTO_ROTATION_RESTRICTED
189    }
190    this.setOrientation(orientation)
191  }
192
193  /**
194   * 销毁
195   */
196  destroy(): void {
197    this.unbindWindowSizeListener()
198    this.unbindFoldDisplayModeChange()
199    this.unbindFoldStatusChange()
200    this.unbindWindowEventListener()
201  }
202
203  /**
204   * 取消监听应用窗口焦点变化
205   */
206  unbindWindowEventListener(): void {
207    // todo: onDestroy的时候窗口已经销毁了,目前没有合适的元能力生命周期用于解注册监听,窗口已知的一个现象,后续解决再放开
208    // try {
209    //   windowClass.off('windowEvent');
210    // } catch (exception) {
211    //   logger.error('Failed to unregister callback. Cause: ' + JSON.stringify(exception));
212    //  }
213  }
214
215  /**
216   * 绑定窗口变化监听
217   */
218  async bindWindowSizeListener(): Promise<void> {
219    let w = await this.windowStage.getMainWindow()
220    if (!w) {
221      return
222    }
223    // 绑定窗口变化监听
224    w.on('windowSizeChange', (data: window.Size) => {
225      logger.info('windowSizeChange' + JSON.stringify(data))
226      this.windowResizeHapenned(data)
227    })
228    w.on('avoidAreaChange', (data) => {
229      if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
230        logger.info('avoidAreaChange' + JSON.stringify(data.area))
231        let isPC = isWideService()
232        this.topHeight = data.area.topRect.height
233
234        this.bottomHeight = data.area.bottomRect.height
235        if (isPC && this.topHeight === 0) {
236          this.topHeight = PC_TOP_PADDING * this.densityDPI
237        }
238        setOrCreateAppStorage<number>(appStorageKeys.statusBarHeight, this.topHeight)
239        setOrCreateAppStorage<number>(appStorageKeys.navigatorBarHeight, this.bottomHeight)
240
241      }
242    })
243    // 绑定窗口模式变化监听
244    w.on('windowStatusChange', (windowStatusChange: window.WindowStatusType) => {
245      logger.info('windowStatusChange: ' + JSON.stringify(windowStatusChange))
246      this.windowStatusType = windowStatusChange
247      if (windowStatusChange === window.WindowStatusType.MINIMIZE) {
248        return
249      }
250      if (windowStatusChange === window.WindowStatusType.SPLIT_SCREEN) {
251        this.setSplitRatio()
252      } else {
253        this.setAppSplitRatio(AppSplitRatios.NO)
254      }
255    })
256  }
257
258  /**
259   * 注销窗口变化监听
260   */
261  async unbindWindowSizeListener(): Promise<void> {
262    let w = await this.windowStage.getMainWindow()
263    if (!w) {
264      return
265    }
266    // 和zhengjiangliang OFF不传callback整体注销
267    await w.off('windowSizeChange')
268    await w.off('avoidAreaChange')
269    await w.off('windowStatusChange')
270  }
271
272  /**
273   * 绑定折叠屏折叠状态变化监听
274   */
275  bindFoldStatusChange(): void {
276
277    try {
278      display.on('foldStatusChange', (foldStatus: display.FoldStatus) => {
279        this.displayStatus = foldStatus;
280        logger.info('displayMode' + foldStatus)
281      });
282    } catch (exception) {
283      logger.error('Failed code' + JSON.stringify(exception));
284    }
285  }
286
287  /**
288   * 注销绑定折叠屏折叠状态变化监听
289   */
290  unbindFoldStatusChange(): void {
291    display.off('foldStatusChange')
292  }
293
294  /**
295   * 绑定折叠屏折叠模式变化监听
296   */
297  bindFoldDisplayModeChange(): void {
298    try {
299      display.on('foldDisplayModeChange', (foldDisplayMode: display.FoldDisplayMode) => {
300        logger.info('displayMode' + foldDisplayMode + this.displayMode + this.orientationSetting)
301        this.displayMode = foldDisplayMode;
302        this.setOrientationSetting()
303      });
304    } catch (exception) {
305      logger.error('Failed code' + JSON.stringify(exception));
306    }
307  }
308
309  unbindFoldDisplayModeChange(): void {
310    display.off('foldDisplayModeChange')
311  }
312
313  /**
314   * 设置全局ability上下文
315   */
316  setContext(context: context.UIAbilityContext): void {
317    this.context = context
318  }
319
320  /**
321   * 获取屏幕大小
322   */
323  async getScreenSize(): Promise<boolean> {
324    logger.info('windows getScreenSize')
325    let p1 = display.getDefaultDisplay().then((disp) => {
326      logger.info('getDefaultDisplay:' + JSON.stringify(disp));
327      this.width = disp.width
328      this.height = disp.height
329      this.densityDPI = disp.densityDPI
330    })
331    let w = await this.windowStage.getMainWindow()
332    if (!w) {
333      return new Promise((resolve) => {
334        p1.then(() => {
335          resolve(true)
336        })
337      })
338    }
339    let p2 = w.getProperties().then((prop: window.WindowProperties) => {
340      logger.info('getProperties:' + JSON.stringify(prop));
341      this.windowWidth = prop.windowRect.width
342      this.windowHeight = prop.windowRect.height
343      this.windowPosX = prop.windowRect.left
344      this.windowPosY = prop.windowRect.top
345      this.isLayoutFullScreen = prop.isLayoutFullScreen || false
346      this.isFullScreenWindow = prop.isFullScreen
347    })
348
349    let isPC = isWideService()
350    return isPC ? new Promise((resolve) => {
351      Promise.all([p1, p2]).then(() => {
352        // PC模式下,有可能设置为手机全屏,此时模式如下判断,需要设置窗口高度减去topHeight
353        if (!this.isFullScreen && this.windowWidth === this.width && this.windowHeight === this.height && this.isLayoutFullScreen && !this.isFullScreenWindow) {
354          logger.info('pc topHeight:' + this.topHeight);
355          this.windowHeight -= this.topHeight
356          logger.info('pc windowHeight:' + this.windowHeight);
357        }
358        this.isPCMultiWindow = this.windowHeight === this.height && this.windowWidth !== this.width
359        this.topHeight = PC_TOP_PADDING * this.densityDPI
360        setOrCreateAppStorage<number>(appStorageKeys.statusBarHeight, this.topHeight)
361        if (!this.isLayoutFullScreen && !this.isPCMultiWindow) {
362          // 天枢窗口非全屏模式非分屏模式,顶部38vp,底部和左右各5vp
363          this.windowWidth = this.windowWidth - 10 * this.densityDPI / 160
364          this.windowHeight = this.windowHeight - 43 * this.densityDPI / 160
365        }
366        this.setSplitRatio()
367        resolve(true)
368      })
369    }) : Promise.all([p1, p2]).then(() => {
370      this.setSplitRatio()
371      return true
372    }).catch(() => {
373      return false
374    })
375  }
376
377  /**
378   * 设置全屏和状态栏颜色
379   */
380  async enterImmersion(): Promise<void> {
381    logger.info('windows enterImmersion')
382    // 手机默认设置全屏,平板PC默认设置不全屏。 如果全屏模式发生切换,则置设置沉浸模式标记位为否,重新进入设置
383    // 除PC之外,其他默认设置全屏
384    let isFullScreen = !isWideService()
385    this.isImmersiveSet && (this.isImmersiveSet = isFullScreen === this.isFullScreen)
386    this.isFullScreen = isFullScreen
387    if (this.isImmersiveSet) {
388      return
389    }
390    this.isImmersiveSet = true
391    let w = await this.windowStage.getMainWindow()
392    if (!w) {
393      return
394    }
395    if (this.isFullScreen) {
396      logger.info('this.isFullScreen' + this.isFullScreen)
397      // await w.setFullScreen(this.isFullScreen)
398      await w.setWindowLayoutFullScreen(this.isFullScreen)
399    }
400    // await w.setSystemBarEnable(["status", "navigation"])
401    this.setSystemBarColor(this.isTopBgDark, this.isBottomBgDark, w)
402    logger.info('windows enterImmersion finish')
403  }
404
405  /**
406   * 设置横竖屏模式
407   *
408   * @param isTopBgDark 顶部背景是否深色
409   * @param isBottomBgDark 底部背景是否深色
410   * @param w topWindow
411   */
412  setSystemBarColor(isTopBgDark: boolean = false, isBottomBgDark: boolean = false, w?: window.Window,): void {
413    this.isTopBgDark = isTopBgDark
414    this.setSystemBarColorWithColor(isTopBgDark ? WHITE : BLACK, isBottomBgDark, w)
415  }
416
417  /**
418   * 设置系统bar颜色
419   *
420   * @param isTopBgDark 顶部背景是否深色
421   * @param isBottomBgDark 底部背景是否深色
422   * @param w topWindow
423   */
424  setSystemBarColorWithColor(statusBarBgColor: string, isBottomBgDark: boolean = false, w?: window.Window): void {
425    if (Util.isEmpty(statusBarBgColor)) {
426      logger.error("setSystemBarColorWithColor :: statusBarBgColor is Empty")
427      return
428    }
429
430    if (statusBarBgColor === this.statusBarBgColor && isBottomBgDark === this.isBottomBgDark) {
431      return
432    }
433    if (!w) {
434      try {
435        w = this.windowStage.getMainWindowSync()
436      } catch (error) {
437        logger.info(`setSystemStatusBarColor, getMainWindowSync error: ${JSON.stringify(error, [`code`, `message`])}`);
438      }
439      if (!w) {
440        return
441      }
442    }
443
444    try {
445      this.statusBarBgColor = statusBarBgColor
446      this.isBottomBgDark = isBottomBgDark
447      w.setWindowSystemBarProperties({
448        navigationBarColor: TRANS,
449        statusBarColor: TRANS,
450        navigationBarContentColor: isBottomBgDark ? WHITE : BLACK,
451        // todo - 如果是深色模式,只需此处逻辑改为 statusBarContentColor: isDark ? ColorUtil.WHITE : statusBarBgColor
452        statusBarContentColor: statusBarBgColor
453      }, null)
454    } catch (error) {
455      logger.error(`setSystemBarColor, setWindowSystemBarProperties error: ${JSON.stringify(error, [`code`, `message`])}`);
456    }
457  }
458
459  /**
460   * 设置横竖屏模式
461   *
462   * @param orientation 横竖屏模式
463   */
464  async setOrientation(orientation: window.Orientation = window.Orientation.AUTO_ROTATION_RESTRICTED): Promise<void> {
465    let w = await this.windowStage.getMainWindow()
466    if (!w) {
467      return
468    }
469    logger.info('this.densityType' + this.densityType + 'this.isFoldScreen' + this.isFoldScreen + 'this.displayMode' + this.displayMode)
470    logger.info('windowResize densityType' + this.orientationSetting + 'orientation' + orientation)
471    if (this.orientationSetting === orientation) {
472      return
473    }
474    this.orientationSetting = orientation
475    logger.info('windowResize densityType350' + this.orientationSetting + 'orientation' + orientation)
476    await w.setPreferredOrientation(orientation)
477  }
478
479  /**
480   * 获取状态栏和导航栏高度
481   */
482  async getAvoidArea(): Promise<void> {
483    logger.info('windows getAvoidArea')
484    let w = await this.windowStage.getMainWindow()
485    if (!w) {
486      return
487    }
488    let area = await w.getAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
489    logger.info('windows getAvoidArea' + JSON.stringify(area))
490    let isPC = isWideService()
491    // PC模式下,如果头部没有空隙,留一定空隙
492    if (isPC && area.topRect.height === 0) {
493      this.topHeight = PC_TOP_PADDING * this.densityDPI
494      // fix systemUI bug 每次下拉系统通知栏,状态栏高度会为0 需要屏蔽掉
495    } else if (area.topRect.height !== 0) {
496      this.topHeight = area.topRect.height
497    }
498    this.bottomHeight = area?.bottomRect?.height || 0
499    logger.info('windows getAvoidArea finish')
500  }
501
502  /**
503   * 窗口变化发生
504   *
505   * @param data 窗口数据
506   */
507  windowResizeHapenned(data: window.Size): void {
508    logger.info('Succeeded in enabling the listener for window size changes. Data: ' + JSON.stringify(data));
509    this.lastWindowSizeResult = this.windowResize()
510  }
511
512  /**
513   * 处理窗口变化修改成员变量属性值
514   *
515   * @return Promise<boolean> 窗口变化结果,成功失败
516   */
517  windowResize(): Promise<boolean> {
518    return new Promise(async (resolve, reject) => {
519      // todo 当前设置全屏后会触发resize,然后又走到这个方法,所以设置全屏和获取屏幕属性方法的时序不需要控制。反正resize之后还有重新获取屏幕属性
520      let p2 = this.getAvoidArea()
521      let p3 = this.getScreenSize()
522      Promise.all([p2, p3]).then(() => {
523        this.windowResizeDetail()
524        resolve(true)
525      }).catch(() => {
526        resolve(false)
527      })
528    })
529  }
530
531  /**
532   * 处理窗口变化修改成员变量属性值具体方法
533   */
534  windowResizeDetail: () => void = () => {
535    if (this.densityDPI === 0) {
536      logger.info('densityDPI zero')
537      return
538    }
539    // 说明计算公式pixels = dips * (density / 160)
540    let vpWidth = this.windowWidth / (this.densityDPI / 160)
541    let densityType: DensityTypes = DensityTypes.SM
542    if (vpWidth < 320) {
543      densityType = DensityTypes.XS
544    } else if (vpWidth < 600) {
545      densityType = DensityTypes.SM
546    } else if (vpWidth < 840) {
547      densityType = DensityTypes.MD
548    } else {
549      densityType = DensityTypes.LG
550    }
551    logger.info('windowResize densityType' + densityType + 'vpWidth' + vpWidth)
552    this.setDensityType(densityType)
553    this.setOrientationSetting()
554  }
555  /**
556   * 计算分屏模式下比例
557   *
558   * @param listener 窗口变化监听器
559   */
560  setSplitRatio: () => void = (): void => {
561    let appSplitRatio: AppSplitRatios
562    if (this.windowStatusType !== window.WindowStatusType.SPLIT_SCREEN) {
563      // 应用宽高与窗口宽高未分屏
564      logger.info('windowResize appSplitRatio none')
565      appSplitRatio = AppSplitRatios.NO
566      return
567    } else if (this.windowWidth === this.width) {
568      // 应用宽度 = 窗口宽度:上下分屏,通过计算 应用高度与窗口高度比值 判断 音乐分屏比例
569      if (this.windowHeight <= this.height * SPLIT_RATIO_MINI) {
570        appSplitRatio = AppSplitRatios.PORTRAIT_S
571      } else if (this.windowHeight > this.height * SPLIT_RATIO_MAX) {
572        appSplitRatio = AppSplitRatios.PORTRAIT_L
573      } else {
574        appSplitRatio = AppSplitRatios.PORTRAIT_M
575      }
576    } else if (this.windowHeight === this.height) {
577      // 应用高度 = 窗口高度:左右分屏,通过计算 应用宽度与窗口宽度比值 判断 音乐分屏比例
578      if (this.windowWidth <= this.width * SPLIT_RATIO_MINI) {
579        appSplitRatio = AppSplitRatios.LANDSCAPE_S
580      } else if (this.windowWidth > this.width * SPLIT_RATIO_MAX) {
581        appSplitRatio = AppSplitRatios.LANDSCAPE_L
582      } else {
583        appSplitRatio = AppSplitRatios.LANDSCAPE_M
584      }
585    } else {
586      appSplitRatio = AppSplitRatios.NO
587    }
588    logger.info(`windowResize appSplitRatio ${appSplitRatio} ${this.windowWidth} ${this.width} ${this.windowHeight} ${this.height}`)
589    this.setAppSplitRatio(appSplitRatio)
590  }
591  /**
592   * 设置屏幕密度类型
593   *
594   * @param type 屏幕密度类型
595   */
596  setDensityType: (type: DensityTypes) => void = (type: DensityTypes): void => {
597    this.densityType = type
598    let windowArea: WindowArea = { windowWidth: this.windowWidth, windowHeight: this.windowHeight }
599    setOrCreateAppStorage<DensityTypes>(appStorageKeys.densityType, type)
600    setOrCreateAppStorage<number>(appStorageKeys.screenWidth, this.width)
601    setOrCreateAppStorage<number>(appStorageKeys.screenHeight, this.height)
602    setOrCreateAppStorage<number>(appStorageKeys.windowWidth, this.windowWidth)
603    setOrCreateAppStorage<number>(appStorageKeys.windowHeight, this.windowHeight)
604    // 保证横竖屏切换两个同时更新
605    setOrCreateAppStorage<WindowArea>(appStorageKeys.windowArea, windowArea)
606    setOrCreateAppStorage<number>(appStorageKeys.statusBarHeight, this.topHeight)
607    setOrCreateAppStorage<number>(appStorageKeys.navigatorBarHeight, this.bottomHeight)
608    if (this.configChangeListener.length > 0) {
609      this.configChangeListener.forEach((listener) => {
610        try {
611          listener(type)
612        } catch (e) {
613          logger.info('setDensityType forEach error = ' + e);
614        }
615      })
616    }
617  }
618  /**
619   * 设置分屏比例
620   *
621   * @param ratio 分屏比例
622   */
623  setAppSplitRatio: (ratio: AppSplitRatios) => void = (ratio: AppSplitRatios): void => {
624    this.appSplitRatio = ratio
625    setOrCreateAppStorage<AppSplitRatios>(appStorageKeys.appSplitRatio, ratio)
626  }
627
628  /**
629   * 设置鼠标形状
630   * @param pointerStyle 鼠标形状
631   */
632  async setPointerStyle(pointerTypes: pointer.PointerStyle): Promise<void> {
633    let w = await this.windowStage.getMainWindow()
634    w.getProperties().then((prop: window.WindowProperties) => {
635      let windowId = prop.id;
636      if (windowId < 0) {
637        logger.info(`Invalid windowId`);
638        return;
639      }
640      try {
641        pointer.setPointerStyle(windowId, pointerTypes).then(() => {
642          logger.info(`Set pointer style success`);
643        });
644      } catch (error) {
645        logger.info(`Set pointer style failed, error: ${JSON.stringify(error, [`code`, `message`])}`);
646      }
647    })
648  }
649
650  /**
651   * 设置/取消屏幕常亮
652   * @param screenOn
653   */
654  async setIsKeepScreenOn(screenOn: boolean): Promise<void> {
655    logger.info('setIsKeepScreenOn ' + screenOn + ' this.isKeepScreenOn ' + this.isKeepScreenOn)
656    if (screenOn === this.isKeepScreenOn) {
657      return
658    }
659
660    let w = await this.windowStage.getMainWindow()
661    if (!w) {
662      logger.info('setIsKeepScreenOn return')
663      return
664    }
665    try {
666      w.setWindowKeepScreenOn(screenOn, (err) => {
667        if (err.code) {
668          logger.error('Failed to set the screen to be always on. Cause: ' + JSON.stringify(err));
669          return;
670        }
671        this.isKeepScreenOn = screenOn
672        logger.info('Succeeded in setting the screen to be always on.' + screenOn);
673      });
674    } catch (exception) {
675      logger.error('Error to set the screen to be always on. Cause: ' + JSON.stringify(exception));
676    }
677  }
678}
679
680export function isWideService(): boolean {
681  const globalDeviceInfo: DeviceInfo = createOrGet(DeviceInfo, globalKeys.deviceInfo)
682  // return globalDeviceInfo.deviceType === DeviceTypes.TABLET || globalDeviceInfo.deviceType === DeviceTypes.PC
683  return globalDeviceInfo.deviceType === DeviceTypes.PC
684}
685
686/**
687 * 设备类型
688 */
689export enum DeviceTypes {
690  PHONE = 0,
691  TABLET = 1,
692  PC = 2
693}
694
695
696/**
697 * 渠道类型
698 */
699export enum EnvironmentType {
700  // 联调环境
701  DEV = 0,
702  // 镜像环境
703  MIRROR = 1,
704  // 现网环境
705  SECURITY = 3
706}
707
708/**
709 * 屏幕密度类型
710 */
711export enum DensityTypes {
712  // 手表
713  XS = 0,
714  // 手机竖屏和折叠屏不展开
715  SM = 1,
716  // 折叠屏展开和pad竖屏
717  MD = 2,
718  // pad横屏
719  LG = 3
720}
721
722/**
723 * 分屏模式下音乐与其他应用所占屏幕比例
724 */
725export enum AppSplitRatios {
726  // 不分屏
727  NO = 0,
728  // 上下分屏 音乐与其他应用屏幕占比 <= 1:2
729  PORTRAIT_S = 1,
730  // 上下分屏 音乐与其他应用屏幕占比 1:2 ~ 2:1
731  PORTRAIT_M = 2,
732  // 上下分屏 音乐与其他应用屏幕占比 >= 2:1
733  PORTRAIT_L = 3,
734  // 左右分屏 音乐与其他应用屏幕占比 <= 1:2
735  LANDSCAPE_S = 4,
736  // 左右分屏 音乐与其他应用屏幕占比 1:2 ~ 2:1
737  LANDSCAPE_M = 5,
738  // 左右分屏 音乐与其他应用屏幕占比 >= 2:1
739  LANDSCAPE_L = 6,
740}
741
742
743/**
744 * 设备信息
745 */
746export class DeviceInfo {
747  // 设备类型
748  deviceType: number = DeviceTypes.PHONE
749  /**
750   * Obtains the product model represented by a string.
751   *
752   * @syscap SystemCapability.Startup.SystemInfo
753   * @since 6
754   */
755  productModel: string
756  /**
757   * Obtains the OS version represented by a string.
758   *
759   * @syscap SystemCapability.Startup.SystemInfo
760   * @since 6
761   */
762  osFullName: string
763  /**
764   * Obtains the SDK API version number.
765   *
766   * @syscap SystemCapability.Startup.SystemInfo
767   * @since 6
768   */
769  sdkApiVersion: number
770  /**
771   * Obtains the device udid.
772   *
773   * @syscap SystemCapability.Startup.SystemInfo
774   * @since 7
775   */
776  udid: string
777  /**
778   * Obtains the device manufacturer represented by a string.
779   *
780   * @syscap SystemCapability.Startup.SystemInfo
781   * @since 6
782   */
783  manufacture: string
784  /**
785   * Obtains the device brand represented by a string.
786   *
787   * @syscap SystemCapability.Startup.SystemInfo
788   * @since 6
789   */
790  brand: string
791  /**
792   * User-Agent
793   *
794   */
795  ua: string = ''
796  /**
797   * Obtains the major (M) version number, which increases with any updates to the overall architecture.
798   * <p>The M version number monotonically increases from 1 to 99.
799   *
800   * @syscap SystemCapability.Startup.SystemInfo
801   * @since 6
802   */
803  majorVersion: number
804  // 系统语言
805  lang: string = MusicApp.DEFAULT_LANG
806  /**
807   * 获取CPU的核数 todo
808   *
809   * @return CPU的核数,异常情况下CPU核数为-1
810   */
811  cpucores: string = '8'
812  memory: string = '8.0G'
813
814  constructor() {
815    //     * which can be {@code phone} (or {@code default} for phones), {@code wearable}, {@code liteWearable},
816    //     * {@code tablet}, {@code tv}, {@code car}, or {@code smartVision}.
817    switch (deviceInfo.deviceType) {
818      case 'phone':
819        this.deviceType = DeviceTypes.PHONE
820        break;
821      case 'default':
822        this.deviceType = DeviceTypes.PHONE
823        break;
824      case 'tablet':
825        this.deviceType = DeviceTypes.TABLET
826        break;
827      case '2in1':
828        this.deviceType = DeviceTypes.PC
829        break;
830      case 'pc':
831        this.deviceType = DeviceTypes.PC
832        break;
833      default:
834        this.deviceType = DeviceTypes.PHONE
835        break;
836    }
837    this.osFullName = deviceInfo.osFullName
838    this.sdkApiVersion = deviceInfo.sdkApiVersion
839    // this.udid = deviceInfo.udid 权限不满足获取不到
840    this.udid = ''
841    this.manufacture = deviceInfo.manufacture
842    this.majorVersion = deviceInfo.majorVersion
843    this.brand = (deviceInfo.brand).toUpperCase()
844    this.productModel = deviceInfo.productModel
845    display.getDefaultDisplay().then((disp) => {
846      logger.info('getDefaultDisplay in device:' + JSON.stringify(disp));
847      this.ua = `model=${this.productModel},brand=${this.brand},rom=${this.osFullName},emui=${this.osFullName},os=${this.majorVersion},apilevel=${this.sdkApiVersion},manufacturer=${this.brand},useBrandCust=0,extChannel=,cpucore=8,memory=8.0G,srceenHeight=${disp.width},screenWidth=${disp.height},harmonyApiLevel=${this.sdkApiVersion},huaweiOsBrand=harmony`
848      logger.debug('deviceType ua:' + this.ua)
849    })
850    // todo 需要确认udid是否可以在同意协议之前获取。
851    logger.debug('deviceType: ' + deviceInfo.deviceType + ';osFullName: ' + deviceInfo.osFullName + ';sdkApiVersion: ' + deviceInfo.sdkApiVersion + ';udid: ' + (deviceInfo.udid !== undefined) + ';manufacture: ' + deviceInfo.manufacture + ';brand: ' + deviceInfo.brand + ';hardwareProfile:' + deviceInfo.hardwareProfile)
852    this.getLang()
853  }
854
855  /**
856   * 获取系统语言
857   *
858   * @return 语言是否发生切换
859   */
860  getLang(): boolean {
861    let lang = i18n.getSystemLocale()
862    let langRes = ''
863    // en-Latn-US 去除Latn
864    if (lang) {
865      let langArray = lang.split('-')
866      if (langArray.length === 3) {
867        langRes = langArray[0] + '-' + langArray[2]
868      } else {
869        langRes = lang
870      }
871    } else {
872      langRes = MusicApp.DEFAULT_LANG
873    }
874    let res = langRes !== this.lang
875    this.lang = langRes
876    // 语言发生变化时更新全局变量
877    if (res) {
878      setOrCreateAppStorage<string>(appStorageKeys.lang, this.lang)
879    }
880    logger.info("i18n getSystemLanguage: " + this.lang)
881    return res
882  }
883}
884
885/**
886 * BEARER_CELLULAR    0    蜂窝网络。
887 * BEARER_WIFI    1    Wi-Fi网络。
888 * BEARER_ETHERNET    3    以太网网络。
889 */
890enum NetworkType {
891  // 蜂窝网络
892  BEARER_CELLULAR = 0,
893  // Wi-Fi网络
894  BEARER_WIFI = 1,
895  // 以太网网络。
896  BEARER_ETHERNET = 3
897}
898
899/**
900 * 应用信息
901 */
902export class MusicApp {
903  // 客户端traceId
904  xClientTraceId: string
905  static readonly DEFAULT_LANG: string = 'zh_CN'
906  // abilityStage上下文
907  abilityStageContext: context.AbilityStageContext | undefined = undefined
908  // ability上下文
909  abilityContext: context.UIAbilityContext | null = null
910  // UIExtensionContext上下文
911  UIExtensionContext: context.UIExtensionContext | null = null
912  // 产品类型名称 phone、watch
913  productName: string
914  // 應用是否在前台
915  active: boolean = true
916  activeNo: number = 0
917  activeChangeListeners: Array<(active: boolean) => void> = []
918
919  constructor(productName: string = 'phone') {
920    this.productName = productName
921    this.xClientTraceId = Util.systemUUid()
922    logger.info("Music app init productName:" + this.productName)
923  }
924
925  /**
926   * 注册應用是否在前台
927   * @param listener listener
928   */
929  addActiveChangeListener(listener: (active: boolean) => void): void {
930    this.activeChangeListeners.push(listener)
931  }
932
933  /**
934   * 取消注册應用是否在前台
935   * @param listener listener
936   */
937  removeActiveChangeListener(listener: (active: boolean) => void): void {
938    let idx = this.activeChangeListeners.findIndex((item) => {
939      return item === listener
940    })
941    if (idx > -1) {
942      this.activeChangeListeners.splice(idx, 1)
943    }
944  }
945
946  /**
947   * 修改應用是否在前台
948   * @param active active
949   */
950  setActive(active: boolean): void {
951    this.active = active
952    if (this.activeChangeListeners.length) {
953      this.activeChangeListeners.forEach((listener: (active: boolean) => void) => {
954        listener(active)
955      })
956    }
957  }
958
959  /**
960   * 修改重复退出回到应用次数
961   * @param active active
962   */
963  setActiveNo(): void {
964    this.activeNo++
965    logger.info("activeNo " + this.activeNo)
966  }
967}
968
969/**
970 * 宽高同时更新
971 */
972export class WindowArea {
973  windowWidth: number = 0
974  windowHeight: number = 0
975}
976
977