• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023-2024 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
16const TAG = 'avcastpicker_component ';
17
18/**
19 * Definition of av cast picker state.
20 */
21export enum AVCastPickerState {
22  /**
23   * The picker starts showing.
24   */
25  STATE_APPEARING,
26
27  /**
28   * The picker finishes presenting.
29   */
30  STATE_DISAPPEARING
31}
32
33/**
34 * Definition of av cast picker state.
35 */
36export enum AVCastPickerStyle {
37  /**
38   * The picker shows in a panel style.
39   */
40  STYLE_PANEL,
41
42  /**
43   * The picker shows in a menu style.
44   */
45  STYLE_MENU
46}
47
48enum DeviceSource {
49  /**
50   * local device
51   */
52  LOCAL,
53
54  /**
55   * cast device
56   */
57  CAST
58}
59
60enum ConfigurationColorMode {
61  /**
62   * the color mode is not set.
63   */
64  COLOR_MODE_NOT_SET = -1,
65
66  /**
67   * Dark mode.
68   */
69  COLOR_MODE_DARK = 0,
70
71  /**
72   * Light mode.
73   */
74  COLOR_MODE_LIGHT = 1
75}
76
77enum AVCastPickerColorMode {
78  /**
79   * the color mode of picker is not set.
80   */
81  AUTO = 0,
82
83  /**
84   * Dark mode of picker.
85   */
86  DARK = 1,
87
88  /**
89   * Light mode of picker.
90   */
91  LIGHT = 2
92}
93
94/**
95 * menuItem device info
96 */
97export interface AVCastPickerDeviceInfo {
98  deviceId: number | String,
99  deviceType: number,
100  deviceName: string,
101  deviceIconName: string,
102  isConnected: boolean,
103  selectedIconName: string,
104  deviceSource: DeviceSource
105}
106
107@Component
108export struct AVCastPicker {
109  /**
110   * Assigns the color of picker component at normal state.
111   */
112  @State normalColor: Color | number | string | undefined = undefined;
113
114  /**
115   * Assigns the color of picker component at active state.
116   */
117  @State activeColor: Color | number | string | undefined = undefined;
118
119  /**
120   * Definition of color mode of picker.
121   */
122  @State colorMode: AVCastPickerColorMode = AVCastPickerColorMode.AUTO;
123
124  /**
125   * The device that is displayed in the menu.
126   */
127  @State deviceList: Array<AVCastPickerDeviceInfo> = [];
128
129  /**
130   * Session type transferred by the application.
131   */
132  @State sessionType: string = 'audio';
133
134  /**
135   * Display form of application transfer.
136   */
137  @State pickerStyle: AVCastPickerStyle = AVCastPickerStyle.STYLE_PANEL;
138
139  /**
140   * Display form mediaController.
141   */
142  @State pickerStyleFromMediaController: AVCastPickerStyle = AVCastPickerStyle.STYLE_PANEL;
143
144  /**
145   * Whether to display the menu.
146   */
147  @State@Watch('MenuStateChange') isMenuShow: boolean = false;
148
149  /**
150   * Touch item index.
151   */
152  @State touchMenuItemIndex: number = -1;
153
154  /**
155   * Picker state change callback.
156   */
157  private onStateChange?: (state: AVCastPickerState) => void;
158
159  /**
160   * UIExtensionProxy.
161   */
162  private extensionProxy: UIExtensionProxy | null = null;
163
164  private pickerClickTime: number = -1;
165
166  /**
167   * Custom builder from application.
168   */
169  @BuilderParam customPicker: (() => void)
170
171  /**
172   * Configuration color mode.
173   */
174  @State configurationColorMode: number = ConfigurationColorMode.COLOR_MODE_NOT_SET;
175
176  @State deviceInfoType: string = '';
177
178  /**
179   * Max Font and graphic magnification.
180   */
181  @State maxFontSizeScale: number = 1;
182
183  /**
184   * Accessibility Strings
185   */
186  @State accessibilityConnectedStr: string = '已连接';
187  @State accessibilityAudioControlStr: string = '音视频投播';
188
189  @State isPc: boolean = false;
190  @State isRTL: boolean = false;
191  @State restartUECMessage: number = 1;
192  private needToRestart: boolean = false;
193  @State isShowLoadingProgress: boolean = false;
194
195  MenuStateChange() {
196    if (this.extensionProxy != null) {
197      this.extensionProxy.send({ 'isMenuShow': this.isMenuShow });
198    }
199  }
200
201  build() {
202    Column() {
203      if (this.customPicker === undefined) {
204        this.buildDefaultPicker(false);
205      } else {
206        this.buildCustomPicker();
207      }
208    }.size({width: '100%', height: '100%'})
209  }
210
211  @Builder
212  iconBuilder(item: AVCastPickerDeviceInfo, isSelected: boolean): void {
213    if (this.deviceInfoType === 'true') {
214      SymbolGlyph(!isSelected ? $r(item.deviceIconName) : $r(item.selectedIconName))
215        .fontSize('24vp')
216        .fontColor((isSelected && this.configurationColorMode !== ConfigurationColorMode.COLOR_MODE_DARK) ?
217          [$r('sys.color.comp_background_emphasize')] : [$r('sys.color.icon_primary')])
218        .renderingStrategy(2)
219    } else {
220      Image(!isSelected ? $r(item.deviceIconName) : $r(item.selectedIconName))
221        .width(24)
222        .height(24)
223        .fillColor((isSelected && this.configurationColorMode !== ConfigurationColorMode.COLOR_MODE_DARK) ?
224          $r('sys.color.comp_background_emphasize') : $r('sys.color.icon_primary'))
225    }
226  }
227
228
229  @Builder
230  textBuilder(item: AVCastPickerDeviceInfo) {
231    Text(item.deviceName)
232      .fontSize($r('sys.float.ohos_id_text_size_body2'))
233      .fontColor(item.isConnected ?
234        (this.configurationColorMode !== ConfigurationColorMode.COLOR_MODE_DARK ?
235        $r('sys.color.font_emphasize') : $r('sys.color.font_primary')) :
236        (this.configurationColorMode !== ConfigurationColorMode.COLOR_MODE_DARK ?
237        $r('sys.color.font_primary') : $r('sys.color.font_secondary')))
238      .width(this.isPc ? 254 : 144)
239      .padding({
240        left: 8,
241        top: this.isPc ? 11 : 17,
242        right: 8,
243        bottom: this.isPc ? 11 : 17
244      })
245      .textOverflow({ overflow: TextOverflow.Ellipsis })
246      .maxLines(2)
247      .wordBreak(WordBreak.BREAK_ALL)
248      .maxFontScale(this.maxFontSizeScale)
249      .direction(this.isRTL ? Direction.Rtl : Direction.Ltr)
250  }
251
252  @Builder
253  deviceMenu() {
254    Column() {
255      ForEach(this.deviceList, (item: AVCastPickerDeviceInfo, index) => {
256        Flex({
257          direction: FlexDirection.Column,
258          justifyContent: FlexAlign.SpaceBetween,
259          alignItems: ItemAlign.End
260        }) {
261          Flex({
262            direction: FlexDirection.Row,
263            justifyContent: FlexAlign.SpaceBetween,
264            alignItems: ItemAlign.Center
265          }) {
266            Row() {
267              this.iconBuilder(item, false)
268
269              this.textBuilder(item)
270            }
271            .alignItems(VerticalAlign.Center)
272
273            if (item.isConnected && item.selectedIconName !== null && item.selectedIconName !== undefined) {
274              Row() {
275                this.iconBuilder(item, true)
276              }
277              .alignItems(VerticalAlign.Center)
278              .accessibilityLevel('yes')
279              .accessibilityText(this.accessibilityConnectedStr)
280            }
281          }
282          .constraintSize({ minHeight: this.isPc ? 40 : 48 })
283          .padding({ left: 12, right: 12 })
284          .onTouch((event) => {
285            if (event.type === TouchType.Down) {
286              this.touchMenuItemIndex = index;
287            } else if (event.type === TouchType.Up) {
288              this.touchMenuItemIndex = -1;
289            }
290          })
291          .backgroundColor(this.touchMenuItemIndex === index ?
292            $r('sys.color.interactive_click') : '#00FFFFFF')
293          .borderRadius(this.touchMenuItemIndex === index ?
294            (this.isPc ? $r('sys.float.corner_radius_level2') : $r('sys.float.corner_radius_level8')): 0)
295
296          if (!this.isPc && (index != this.deviceList.length - 1)) {
297            Divider()
298              .height(1)
299              .color($r('sys.color.comp_divider'))
300              .padding({ right: (this.isRTL ? 44 : 12), left: (this.isRTL ? 12 : 44) })
301          } else if (this.isPc && (index != this.deviceList.length - 1)) {
302            Row()
303              .width('100%')
304              .height(2)
305          }
306        }
307        .width('100%')
308        .onClick(() => {
309          if (this.extensionProxy != null && !item.isConnected) {
310            this.extensionProxy.send({ 'selectedDeviceInfo': item })
311          }
312        })
313      })
314    }
315    .width(this.isPc ? 326 : 216)
316    .borderRadius(this.isPc ? 8 : 20)
317  }
318
319  @Builder
320  private buildDefaultPicker(isCustomPicker: boolean) {
321    Button() {
322      UIExtensionComponent(
323      {
324        abilityName: 'UIExtAbility',
325        bundleName: 'com.hmos.mediacontroller',
326        parameters: {
327          'normalColor': this.normalColor,
328          'activeColor': this.activeColor,
329          'pickerColorMode': this.colorMode,
330          'avCastPickerStyle': this.pickerStyle,
331          'ability.want.params.uiExtensionType': 'sysPicker/mediaControl',
332          'isCustomPicker': isCustomPicker,
333          'message': this.restartUECMessage,
334        }
335      })
336      .onRemoteReady((proxy: UIExtensionProxy) => {
337        console.info(TAG, 'onRemoteReady');
338        this.extensionProxy = proxy;
339      })
340      .onReceive((data) => {
341        if (JSON.stringify(data['deviceInfoType']) !== undefined) {
342          console.info(TAG, `deviceInfoType : ${JSON.stringify(data['deviceInfoType'])}`);
343          this.deviceInfoType = data['deviceInfoType'] as string;
344        }
345
346        if (JSON.stringify(data['pickerStyle']) !== undefined) {
347          console.info(TAG, `picker style : ${JSON.stringify(data['pickerStyle'])}`);
348          this.pickerStyleFromMediaController = data['pickerStyle'] as AVCastPickerStyle;
349        }
350
351        if (JSON.stringify(data['deviceList']) !== undefined) {
352          console.info(TAG, `picker device list : ${JSON.stringify(data['deviceList'])}`);
353          this.deviceList = JSON.parse(JSON.stringify(data['deviceList']));
354          let hasOnlySpeakerAndEarpiece: boolean = this.deviceList.length === 2 && !this.hasExtDevice(this.deviceList);
355          let hasNoDevices: boolean = this.deviceList === null || this.deviceList.length === 0;
356          let isCalling: boolean = this.sessionType === 'voice_call' || this.sessionType === 'video_call';
357          let isExtMenuScene = isCalling && (hasNoDevices || hasOnlySpeakerAndEarpiece);
358          let isPanelForMedia: boolean = !isCalling &&
359            (this.pickerStyle === AVCastPickerStyle.STYLE_PANEL &&
360            this.pickerStyleFromMediaController === AVCastPickerStyle.STYLE_PANEL);
361          if (isExtMenuScene || isPanelForMedia) {
362            this.isMenuShow = false;
363            this.touchMenuItemIndex = -1;
364          }
365        }
366
367        if (JSON.stringify(data['state']) !== undefined) {
368          console.info(TAG, `picker state change : ${JSON.stringify(data['state'])}`);
369          let isCalling: boolean = (this.sessionType === 'voice_call' || this.sessionType === 'video_call');
370          let isPanelForMedia: boolean = !isCalling &&
371            (this.pickerStyle === AVCastPickerStyle.STYLE_PANEL &&
372            this.pickerStyleFromMediaController === AVCastPickerStyle.STYLE_PANEL);
373          if (this.onStateChange != null && isPanelForMedia) {
374            if (parseInt(JSON.stringify(data['state'])) === AVCastPickerState.STATE_APPEARING) {
375              this.onStateChange(AVCastPickerState.STATE_APPEARING);
376            } else {
377              this.onStateChange(AVCastPickerState.STATE_DISAPPEARING);
378            }
379          }
380        }
381
382        if (JSON.stringify(data['sessionType']) !== undefined) {
383          console.info(TAG, `session type : ${JSON.stringify(data['sessionType'])}`);
384          this.sessionType = data['sessionType'] as string;
385        }
386
387        if (JSON.stringify(data['isShowMenu']) !== undefined) {
388          console.info(TAG, `isShowMenu : ${JSON.stringify(data['isShowMenu'])}`);
389          this.isMenuShow = data['isShowMenu'] as boolean;
390          if (!this.isMenuShow) {
391            this.touchMenuItemIndex = -1;
392          }
393        }
394
395        if (JSON.stringify(data['configurationColorMode']) !== undefined) {
396          console.info(TAG, `configurationColorMode : ${JSON.stringify(data['configurationColorMode'])}`);
397          this.configurationColorMode = data['configurationColorMode'] as number;
398        }
399
400        if (JSON.stringify(data['accessConnected']) !== undefined) {
401          console.info(TAG, `accessConnected : ${JSON.stringify(data['accessConnected'])}`);
402          this.accessibilityConnectedStr = data['accessConnected'] as string;
403        }
404
405        if (JSON.stringify(data['accessAudioControl']) !== undefined) {
406          console.info(TAG, `accessAudioControl : ${JSON.stringify(data['accessAudioControl'])}`);
407          this.accessibilityAudioControlStr = data['accessAudioControl'] as string;
408        }
409
410        if (JSON.stringify(data['isPc']) !== undefined) {
411          console.info(TAG, `isPc : ${JSON.stringify(data['isPc'])}`);
412          this.isPc = data['isPc'] as boolean;
413        }
414
415        if (JSON.stringify(data['isRTL']) !== undefined) {
416          console.info(TAG, `isRTL : ${JSON.stringify(data['isRTL'])}`);
417          this.isRTL = data['isRTL'] as boolean;
418        }
419
420        if (JSON.stringify(data['maxFontSizeScale']) !== undefined) {
421          console.info(TAG, `maxFontSizeScale : ${JSON.stringify(data['maxFontSizeScale'])}`);
422          this.maxFontSizeScale = data['maxFontSizeScale'] as number;
423        }
424
425        if (JSON.stringify(data['isShowLoadingProgress']) !== undefined) {
426          console.info(TAG, `isShowLoadingProgress : ${JSON.stringify(data['isShowLoadingProgress'])}`);
427          this.isShowLoadingProgress = data['isShowLoadingProgress'] as boolean;
428        }
429      })
430      .size({ width: '100%', height: '100%' })
431      .bindMenu(this.isMenuShow, this.deviceMenu(), {
432        placement: Placement.BottomRight,
433        onDisappear: () => {
434          this.isMenuShow = false;
435          this.touchMenuItemIndex = -1;
436          this.menuShowStateCallback(this.isMenuShow);
437        },
438        onAppear: () => {
439          if (this.extensionProxy != null && this.pickerClickTime !== -1) {
440              this.extensionProxy.send({ 'timeCost': new Date().getTime() - this.pickerClickTime});
441              this.pickerClickTime = -1;
442          }
443          this.menuShowStateCallback(this.isMenuShow);
444        }
445      })
446      .onRelease((releaseCode) => {
447        if (releaseCode === 1) {
448          this.needToRestart = true;
449        }
450      })
451      .onError(() => {
452        this.needToRestart = true;
453      })
454      .accessibilityLevel('yes')
455      .accessibilityText(this.accessibilityAudioControlStr)
456    }
457    .backgroundColor('#00000000')
458    .size({ width: '100%', height: '100%' })
459    .accessibilityLevel('yes')
460    .accessibilityText(this.accessibilityAudioControlStr)
461    .onClick(() => {
462        if (this.needToRestart) {
463          this.needToRestart = false;
464          this.restartUECMessage += 1;
465          return;
466        }
467        let hasOnlySpeakerAndEarpiece: boolean = this.deviceList.length === 2 && !this.hasExtDevice(this.deviceList);
468        let hasNoDevices: boolean = this.deviceList === null || this.deviceList.length === 0;
469        let isCalling: boolean = this.sessionType === 'voice_call' || this.sessionType === 'video_call';
470        let isExtMenuScene: boolean = isCalling && (hasNoDevices || hasOnlySpeakerAndEarpiece);
471        let isPanelForMedia: boolean = !isCalling &&
472          (this.pickerStyle === AVCastPickerStyle.STYLE_PANEL &&
473          this.pickerStyleFromMediaController === AVCastPickerStyle.STYLE_PANEL);
474        if (isExtMenuScene || isPanelForMedia) {
475          this.isMenuShow = false;
476          this.touchMenuItemIndex = -1;
477          if (this.extensionProxy != null) {
478            this.extensionProxy.send({'clickEvent': true});
479          }
480        } else {
481          this.isMenuShow = !this.isMenuShow;
482          if (this.isMenuShow) {
483            this.pickerClickTime = new Date().getTime();
484          } else {
485            this.touchMenuItemIndex = -1;
486          }
487        }
488      })
489  }
490
491  private hasExtDevice(allDevice: Array<AVCastPickerDeviceInfo>): boolean {
492    for (let i = 0; i < allDevice.length; i++) {
493      if (allDevice[i].deviceType !== 1 && // 1 is audio.DeviceType.EARPIECE
494        allDevice[i].deviceType !== 2) { // 2 is audio.DeviceType.SPEAKER
495        return true;
496      }
497    }
498    return false;
499  }
500
501  private menuShowStateCallback(isMenuShow: boolean): void {
502    if (this.onStateChange != null &&
503      (this.pickerStyle === AVCastPickerStyle.STYLE_MENU ||
504      this.pickerStyleFromMediaController === AVCastPickerStyle.STYLE_MENU)) {
505      let menuShowState: AVCastPickerState = isMenuShow ?
506        AVCastPickerState.STATE_APPEARING : AVCastPickerState.STATE_DISAPPEARING;
507      this.onStateChange(menuShowState);
508    }
509  }
510
511  @Builder
512  private buildCustomPicker() {
513    Stack({ alignContent: Alignment.Center}) {
514      Column() {
515        if (this.isShowLoadingProgress) {
516          LoadingProgress()
517            .color($r('sys.color.icon_secondary'))
518            .width('20vp')
519            .height('20vp')
520        } else {
521          this.customPicker();
522        }
523      }
524      .alignItems(HorizontalAlign.Center)
525      .justifyContent(FlexAlign.Center)
526      .size({ width: '100%', height: '100%' })
527      .zIndex(0)
528
529      Column() {
530        this.buildDefaultPicker(true);
531      }
532      .alignItems(HorizontalAlign.Center)
533      .justifyContent(FlexAlign.Center)
534      .size({ width: '100%', height: '100%' })
535      .zIndex(1)
536    }
537    .size({ width: '100%', height: '100%' })
538  }
539}