1/*
2 * Copyright (c) 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 */
15import common from '@ohos.app.ability.common';
16import display from '@ohos.display';
17import settings from '@ohos.settings';
18import UIExtensionContentSession from '@ohos.app.ability.UIExtensionContentSession';
19import deviceInfo from '@ohos.deviceInfo';
20import { BusinessError } from '@ohos.base';
21import { EditableLeftIconType, EditableTitleBar } from '@ohos.arkui.advanced.EditableTitleBar';
22import mediaQuery from '@ohos.mediaquery';
23import ConfigurationConstant from '@ohos.app.ability.ConfigurationConstant';
24import CommonConstants from '../common/constants/CommonConstants';
25import { TipsJumpUtils } from '../utils/TipsJumpUtils';
26
27const TAG = '[PasteboardSwitch_Page] : ';
28let context = getContext(this) as common.UIAbilityContext;
29let localStorage = LocalStorage.getShared();
30
31interface switchStatus {
32  open: string;
33  close: string;
34}
35
36let switchState: switchStatus = {
37  open: CommonConstants.SWITCH_STATUS_OPEN,
38  close: CommonConstants.SWITCH_STATUS_CLOSE
39}
40
41@Entry
42@Component
43struct PasteboardSwitch {
44  @StorageLink('isSwitchOn') isSwitchOn: boolean | undefined = true;
45  @StorageLink('pasteboardSession') pasteboardSession: UIExtensionContentSession | undefined = undefined;
46  @State title: string = '';
47  @State screenHeight: number = 0;
48  @State screenWidth: number = 0;
49  @State shortSideSize: number = 0;
50  @State imageAnimatorHeight: number = 0;
51  @State imageAnimatorWidth: number = 0;
52  @State textWidth: number = 0;
53  @State gapLength: number = 0;
54  @State isShow: boolean = false;
55  @State is2in1: boolean = false;
56  @State portraitFunc: mediaQuery.MediaQueryResult | void | null = null;
57  @State contentHeight: number = 0;
58  @State imageArray: Array<ImageFrameInfo> = [];
59  @State animationState: AnimationStatus = AnimationStatus.Running;
60  @State reverse: boolean = false;
61  @State iterations: number = -1;
62  @State isShowBack: boolean = true;
63  private listener: mediaQuery.MediaQueryListener = mediaQuery.matchMediaSync('(dark-mode:true)');
64  private extContext?: common.UIExtensionContext;
65  private scroller: Scroller = new Scroller();
66  private startReason?: string = '';
67  private learnMore: ResourceStr = $r('app.string.learn_more');
68  private pasteboardDesc: ResourceStr = $r('app.string.pasteboard_desc_text', '');
69
70  onPortrait(mediaQueryResult: mediaQuery.MediaQueryResult) {
71    console.log(TAG + 'onPortrait in');
72    if (mediaQueryResult.matches as boolean) {
73      this.extContext?.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_DARK);
74    } else {
75      this.extContext?.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT);
76    }
77  }
78
79  /**
80   * @description Calculate the ImageAnimator size
81   *   Calculation rule:
82   *    1.general phone & pad : screen height/2 compare with width,shorter is ImageAnimator width
83   *    2.pc: screen height/2 compare with width,shorter is ImageAnimator height
84   *    3.ratio: 3:2
85   * @param height
86   * @param width
87   */
88  getImageAnimatorSize(height: Length, width: Length) {
89    console.log(TAG + 'getImageAnimatorSize in, deviceInfo.deviceType : ' + deviceInfo.deviceType);
90    let totalHeight = height as number;
91    this.shortSideSize = width < totalHeight / 2 ? width as number : totalHeight / 2 as number;
92    if (deviceInfo.deviceType === 'phone' || deviceInfo.deviceType === 'tablet') {
93      this.imageAnimatorWidth = this.shortSideSize * 0.8;
94      this.imageAnimatorHeight = this.shortSideSize * 0.8 * 2 / 3;
95    } else if (deviceInfo.deviceType === '2in1') {
96      this.imageAnimatorWidth = this.shortSideSize * 0.8 * 3 / 2;
97      this.imageAnimatorHeight = this.shortSideSize * 0.8;
98    }
99    console.log(TAG + 'this.shortSideSize = ' + this.shortSideSize + ', this.imageAnimatorWidth = ' +
100    this.imageAnimatorWidth + ', this.imageAnimatorHeight = ' + this.imageAnimatorHeight);
101  }
102
103  getStringSync() {
104    console.log(TAG + 'getStringSync in');
105    try {
106      context.resourceManager.getStringValue($r('app.string.pasteboard_title')
107        .id, (error: BusinessError, value: string) => {
108        if (error != null) {
109          console.error(TAG + 'error is ' + error);
110        } else {
111          this.title = value;
112          console.info(TAG + '<aboutToAppear> this.title : ' + this.title);
113        }
114      })
115    } catch (error) {
116      let code: number = (error as BusinessError).code;
117      let message: string = (error as BusinessError).message;
118      console.error(TAG + `callback getStringValue failed,error code: ${code},message: ${message}.`);
119    }
120  }
121
122  getImageArray() {
123    console.info(TAG + 'getImageArray in');
124    for (let i = 1; i <= CommonConstants.IMAGE_COUNT; i++) {
125      this.imageArray.push({
126        src: $r(`app.media.pasteboard_${i}`),
127        duration: (i == CommonConstants.IMAGE_COUNT) ? CommonConstants.IMG_ANIMATOR_OVER_DURATION
128          : CommonConstants.IMG_ANIMATOR_NORMAL_DURATION
129      })
130    }
131  }
132
133  onPageShow() {
134    console.log(TAG + 'onPageShow in');
135    this.getGapLength();
136    display.getAllDisplays((err, data) => {
137      this.screenWidth = px2vp(data[0].width);
138      this.screenHeight = px2vp(data[0].height);
139      this.contentHeight = this.screenHeight;
140      console.log(TAG + 'screenWidth = ' + this.screenWidth + '; screenHeight = ' + this.screenHeight);
141    })
142    this.is2in1 = deviceInfo.deviceType === '2in1' ? true : false;
143  }
144
145  aboutToAppear() {
146    console.log(TAG + 'aboutToAppear in');
147    // Switch State Initialization
148    let value = settings.getValueSync(context, 'distributed_pasteboard_switch', switchState.open);
149    this.isSwitchOn = value != switchState.close ? true : false;
150    console.log(TAG + '<aboutToAppear> this.isSwitchOn : ' + this.isSwitchOn + '; value: ' + value);
151
152    AppStorage.setOrCreate('isSwitchOn', this.isSwitchOn);
153    console.log(TAG + 'AppStorage.get<boolean>(isSwitchOn) : ' + AppStorage.get<boolean>('isSwitchOn'));
154
155    if (this.isSwitchOn) {
156      let status: boolean = settings.setValueSync(context, 'distributed_pasteboard_switch', switchState.open);
157      console.log(TAG + 'set value success :' + status + '; set:distributed_pasteboard_switch is 1');
158    }
159
160    this.getStringSync();
161    this.getImageArray();
162
163    this.listener.on('change', (mediaQueryResult: mediaQuery.MediaQueryResult) => {
164      this.onPortrait(mediaQueryResult);
165    })
166    this.extContext = localStorage.get<common.UIExtensionContext>('context');
167    this.startReason = AppStorage.get<string>('startReason');
168    this.isShowBack = AppStorage.get<boolean>('isShowBack') ?? true;
169    console.info(`${TAG} aboutToAppear: startReason is ${this.startReason}, isShowBack: ${this.isShowBack}`);
170    if (this.isPhone()) {
171      this.checkFoldBackButton();
172    }
173    this.checkPcPadBackButton();
174  }
175
176  aboutToDisappear() {
177    console.info(`${TAG} aboutToDisappear in`);
178  }
179
180  getGapLength() {
181    console.log(TAG + 'getGapLength in, deviceInfo.deviceType : ' + deviceInfo.deviceType);
182    if (deviceInfo.deviceType == 'phone') {
183      this.gapLength = CommonConstants.GENERAL_PHONE_GAP_LENGTH;
184    } else if (deviceInfo.deviceType == '2in1' || deviceInfo.deviceType == 'tablet') {
185      this.gapLength = CommonConstants.PC_PAD_GAP_LENGTH;
186    }
187    console.log(TAG + 'this.gapLength : ' + this.gapLength);
188  }
189
190  onBackPress() {
191    console.log(TAG + 'onBackPress in');
192  }
193
194  @Builder
195  NormalRootContent() {
196    this.titleBar();
197    this.ContentBuilder();
198  }
199
200  @Builder
201  SearchRootContent() {
202    NavDestination() {
203      this.ContentBuilder();
204    }
205    .hideTitleBar(false)
206    .title(this.title)
207    .backgroundColor($r('sys.color.ohos_id_color_titlebar_sub_bg'))
208  }
209
210  @Builder
211  titleBar() {
212    Column() {
213      EditableTitleBar({
214        leftIconStyle: EditableLeftIconType.Back,
215        title: $r('app.string.pasteboard_title'),
216        isSaveIconRequired: false,
217        onCancel: () => {
218          if (this.pasteboardSession) {
219            this.pasteboardSession.sendData({ 'action': 'pop' })
220          } else {
221            console.error(TAG + 'pasteboardSession is undefined');
222          }
223        }
224      })
225    }
226  }
227
228  @Builder
229  ContentBuilder() {
230    Scroll(this.scroller) {
231      Column() {
232        ImageAnimator()
233          .images(this.imageArray)
234          .state(this.animationState)
235          .reverse(this.reverse)
236          .fillMode(this.iterations)
237          .iterations(this.iterations)
238          .width(this.imageAnimatorWidth)
239          .height(this.imageAnimatorHeight)
240          .onStart(() => {
241            console.info(TAG + 'ImageAnimator Start');
242          })
243          .onFinish(() => {
244            console.info(TAG + 'ImageAnimator Finish');
245          })
246
247        Text() {
248          Span(this.pasteboardDesc)
249            .fontFamily('HarmonyHeiTi')
250            .fontSize($r('sys.float.ohos_id_text_size_body2'))
251            .fontColor($r('sys.color.ohos_id_color_text_secondary'))
252            .fontWeight(FontWeight.Regular)
253          Span(this.learnMore)
254            .fontFamily('HarmonyHeiTi')
255            .fontSize($r('sys.float.ohos_id_text_size_body2'))
256            .fontColor($r('sys.color.ohos_id_color_text_primary_activated'))
257            .fontWeight(FontWeight.Medium)
258            .onClick(() => {
259              TipsJumpUtils.jumpTips(getContext(this) as common.UIAbilityContext, CommonConstants.FUN_NUM,
260                CommonConstants.TIPS_TYPE);
261            })
262        }
263        .margin({
264          bottom: CommonConstants.PASTEBOARD_DESC_TEXT_MARGIN_BOTTOM,
265          top: CommonConstants.PASTEBOARD_DESC_TEXT_MARGIN_TOP
266        })
267        .textAlign(TextAlign.Center)
268        .width('100%')
269
270        Column() {
271          Flex({
272            direction: FlexDirection.Row,
273            justifyContent: FlexAlign.SpaceBetween,
274            alignItems: ItemAlign.Center
275          }) {
276            Text($r('app.string.pasteboard_title'))
277              .fontSize($r('sys.float.ohos_id_text_size_sub_title2'))
278              .fontWeight(FontWeight.Medium)
279              .fontColor($r('sys.color.ohos_id_color_text_primary'))
280              .accessibilityLevel('no')
281              .maxFontScale(2.8)
282              .padding({
283                top: this.is2in1 ? CommonConstants.ITEM_LIST_PADDING_TOP_PC : CommonConstants.ITEM_LIST_PADDING_TOP,
284                bottom: this.is2in1 ? CommonConstants.ITEM_LIST_PADDING_BOTTOM_PC :
285                CommonConstants.ITEM_LIST_PADDING_BOTTOM
286              })
287
288            Toggle({ type: ToggleType.Switch, isOn: this.isSwitchOn })
289              .width(CommonConstants.PASTEBOARD_SWITCH_WIDTH)
290              .height(CommonConstants.PASTEBOARD_SWITCH_HEIGHT)
291              .hoverEffect(HoverEffect.None)
292              .onChange((isOn: boolean) => {
293                console.log(TAG + 'isOn:' + isOn);
294                this.isSwitchOn = isOn;
295                AppStorage.setAndLink('isSwitchOn', isOn);
296                if (isOn) {
297                  let status: boolean =
298                    settings.setValueSync(context, 'distributed_pasteboard_switch', switchState.open);
299                  console.log(TAG + 'is set success :' + status + '; set:distributed_pasteboard_switch is on');
300                } else {
301                  let status: boolean =
302                    settings.setValueSync(context, 'distributed_pasteboard_switch', switchState.close);
303                  console.log(TAG + 'is set success :' + status + '; set:distributed_pasteboard_switch is close');
304                }
305              })
306          }
307          .width('100%')
308          .padding({
309            left: CommonConstants.TEXT_LIST_ALIGN_DISTANCE,
310            right: CommonConstants.TEXT_LIST_ALIGN_DISTANCE
311          })
312          .backgroundColor($r('sys.color.ohos_id_color_list_card_bg'))
313          .borderRadius(this.is2in1 ? CommonConstants.PC_BORDER_RADIUS : CommonConstants.NON_PC_BORDER_RADIUS)
314          .accessibilityText(this.title)
315        }
316        .width('100%')
317        .constraintSize({
318          minHeight: CommonConstants.PC_LIST_HEIGHT
319        })
320
321        Column() {
322          Flex({
323            direction: FlexDirection.Row,
324            justifyContent: FlexAlign.Start,
325            alignItems: ItemAlign.Center
326          }) {
327            SymbolGlyph($r('sys.symbol.info_circle_fill'))
328              .fontWeight(FontWeight.Medium)
329              .fontSize(CommonConstants.SYMBOL_INFO_CIRCLE)
330              .fontColor([$r('sys.color.ohos_id_color_activated')])
331              .margin({ right: CommonConstants.SYMBOL_MARGIN_RIGHT })
332
333            Text($r('app.string.update_version_prompt'))
334              .fontSize($r('sys.float.ohos_id_text_size_body3'))
335              .fontWeight(FontWeight.Medium)
336              .fontColor($r('sys.color.ohos_id_color_text_primary'))
337              .textAlign(TextAlign.Start)
338              .lineHeight(CommonConstants.UPDATE_PROMPT_LINE_HEIGHT)
339          }
340          .margin({ top: CommonConstants.UPDATE_PROMPT_MARGIN_TOP })
341        }
342        .padding({
343          left: CommonConstants.TEXT_LIST_ALIGN_DISTANCE,
344          right: CommonConstants.TEXT_LIST_ALIGN_DISTANCE
345        })
346      }
347      .width('100%')
348      .padding({ left: this.gapLength, right: this.gapLength })
349      .margin({ bottom: this.contentHeight * 0.09 })
350      .backgroundColor($r('sys.color.ohos_id_color_sub_background'))
351    }
352    .width('100%')
353    .height(this.screenHeight)
354    .scrollable(ScrollDirection.Vertical)
355    .scrollBar(BarState.Off)
356    .align(Alignment.TopStart)
357    .friction(0.6)
358    .edgeEffect(EdgeEffect.Spring)
359    .onWillScroll((xOffset: number, yOffset: number) => {
360      console.info(TAG + 'onScroll : xOffset:' + xOffset + ' ; yOffset' + yOffset);
361    })
362    .onScrollEdge(() => {
363      console.info('To the edge');
364    })
365    .onScrollStop(() => {
366      console.info('Scroll Stop');
367    })
368    .onAreaChange((oldArea: Area, newArea: Area) => {
369      console.log(TAG + 'Scroll, oldArea.height = ' + oldArea.height + ', newArea.height = ' + newArea.height);
370    })
371  }
372
373  build() {
374    Column() {
375      if (this.isShowBack) {
376        this.NormalRootContent();
377      } else {
378        this.SearchRootContent();
379      }
380    }
381    .width('100%')
382    .height('100%')
383    .backgroundColor($r('sys.color.ohos_id_color_sub_background'))
384    .onAreaChange((oldArea: Area, newArea: Area) => {
385      console.log(TAG + 'build column , oldArea.height = ' + oldArea.height + ', newArea.height = ' + newArea.height);
386      this.getImageAnimatorSize(newArea.height, newArea.width);
387      console.info(TAG + 'this.shortSideSize = ' + this.shortSideSize + ', this.imageAnimatorWidth = ' +
388      this.imageAnimatorWidth + ', this.imageAnimatorHeight = ' + this.imageAnimatorHeight);
389    })
390  }
391
392  private checkPcPadBackButton(): void {
393    console.info(`${TAG} checkPcPadBackButton in`);
394    if (this.startReason === 'from_search') {
395      if (deviceInfo.deviceType === '2in1' || deviceInfo.deviceType === 'tablet') {
396        this.isShowBack = false;
397      }
398    }
399  }
400
401  private isPhone(): boolean {
402    console.info(`${TAG} isPhone in`);
403    return (deviceInfo.deviceType === 'phone' || deviceInfo.deviceType === 'default');
404  }
405
406  private refreshFoldStatus(foldStatus: display.FoldStatus): void {
407    console.info(`${TAG} refreshFoldStatus in. foldStatus: ${foldStatus}. startReason: ${this.startReason}`);
408    if (this.startReason === 'from_search') {
409      if (foldStatus === display.FoldStatus.FOLD_STATUS_FOLDED) {
410        this.isShowBack = true;
411        console.info(`${TAG} foldStatus: ${foldStatus}. this.isShowBack: ${this.isShowBack}`);
412      } else {
413        this.isShowBack = false;
414        console.info(`${TAG} foldStatus: ${foldStatus}. this.isShowBack: ${this.isShowBack}`);
415      }
416    } else {
417      this.isShowBack = true;
418      console.info(`${TAG} startReason: ${this.startReason}. this.isShowBack: ${this.isShowBack}`);
419    }
420  }
421
422  private checkFoldBackButton(): void {
423    console.info(`${TAG} checkFoldBackButton in`);
424    if (display.isFoldable()) {
425      try {
426        display.on('foldStatusChange', (foldStatus: display.FoldStatus) => {
427          let foldStatusValue = foldStatus.valueOf();
428          console.info(`${TAG} checkFoldBackButton: foldStatusValue is ${foldStatusValue}`);
429          this.refreshFoldStatus(foldStatusValue);
430        })
431        let data: display.FoldStatus = display.getFoldStatus();
432        this.refreshFoldStatus(data);
433      } catch (err) {
434        console.error(`${TAG} Register failed. exception: ${err.message}`);
435      }
436    }
437  }
438}