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, { FontSizeScale } from '../common/constants/CommonConstants'; 25import { TipsJumpUtils } from '../utils/TipsJumpUtils'; 26import { LengthMetrics } from '@ohos.arkui.node'; 27import systemParameterEnhance from '@ohos.systemParameterEnhance'; 28 29const TAG = '[PasteboardSwitch_Page] : '; 30let context = getContext(this) as common.UIAbilityContext; 31let localStorage = LocalStorage.getShared(); 32 33interface switchStatus { 34 open: string; 35 close: string; 36} 37 38let switchState: switchStatus = { 39 open: CommonConstants.SWITCH_STATUS_OPEN, 40 close: CommonConstants.SWITCH_STATUS_CLOSE 41} 42 43@Entry 44@Component 45struct PasteboardSwitch { 46 @StorageLink('isSwitchOn') isSwitchOn: boolean | undefined = true; 47 @StorageLink('pasteboardSession') pasteboardSession: UIExtensionContentSession | undefined = undefined; 48 @State title: string = ''; 49 @State screenHeight: number = 0; 50 @State screenWidth: number = 0; 51 @State shortSideSize: number = 0; 52 @State imageAnimatorHeight: number = this.getImageAnimatorHeight(); 53 @State imageAnimatorWidth: number = this.getImageAnimatorWidth(); 54 @State textWidth: number = 0; 55 @State gapLength: number = 0; 56 @State isShow: boolean = false; 57 @State is2in1: boolean = false; 58 @State isBacked: boolean = false; 59 @State isAnimatorDone: boolean = false; 60 @State portraitFunc: mediaQuery.MediaQueryResult | void | null = null; 61 @State contentHeight: number = 0; 62 @State imageArray: Array<ImageFrameInfo> = []; 63 @State animationState: AnimationStatus = AnimationStatus.Running; 64 @State reverse: boolean = false; 65 @State iterations: number = -1; 66 @State isShowBack: boolean = true; 67 @State titleBarMarginNormal: LocalizedMargin = { 68 start: LengthMetrics.resource($r('sys.float.margin_left')), 69 }; 70 @State titleBarMarginPc: LocalizedMargin = { 71 start: LengthMetrics.vp(16), 72 }; 73 @State paddingStartNormal: LengthMetrics = LengthMetrics.resource($r('sys.float.margin_left')); 74 @State paddingStartPc: LengthMetrics = LengthMetrics.vp(24); 75 @StorageProp('currentFontSizeScale') @Watch('onFontSizeScaleChange') fontSizeScale: number = 1; 76 @State phoneSwitchTextTopMargin: string = '17vp'; 77 @State phoneSwitchTextBottomMargin: string = '18vp'; 78 private listener: mediaQuery.MediaQueryListener = mediaQuery.matchMediaSync('(dark-mode:true)'); 79 private extContext?: common.UIExtensionContext; 80 private scroller: Scroller = new Scroller(); 81 private startReason?: string = ''; 82 private learnMore: ResourceStr = $r('app.string.learn_more'); 83 private pasteboardDesc: ResourceStr = $r('app.string.pasteboard_desc_text', ''); 84 85 onPortrait(mediaQueryResult: mediaQuery.MediaQueryResult) { 86 console.log(TAG + 'onPortrait in'); 87 if (mediaQueryResult.matches as boolean) { 88 this.extContext?.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_DARK); 89 } else { 90 this.extContext?.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT); 91 } 92 } 93 94 onFontSizeScaleChange(): void { 95 console.info(`${TAG} onFontSizeScaleChange`); 96 this.phoneSwitchTextTopMargin = this.UpdateMarginBasedOnFontSize(17, this.fontSizeScale); 97 this.phoneSwitchTextBottomMargin = this.UpdateMarginBasedOnFontSize(18, this.fontSizeScale); 98 } 99 100 /** 101 * Update the margins of the switch list according to the font size. 102 */ 103 public UpdateMarginBasedOnFontSize(fontFp: number, fontSizeScale: number): string { 104 console.info(`${TAG} getlistSpace, fontSizeScale: ${fontSizeScale} ; fontFp: ${fontFp}`); 105 switch (fontSizeScale) { 106 case FontSizeScale.XXL1: 107 return '16vp'; 108 case FontSizeScale.XXL2: 109 return '20vp'; 110 case FontSizeScale.XXL3: 111 return '24vp'; 112 default: 113 return `${fontFp}vp`; 114 } 115 } 116 117 /** 118 * Initialize the switch list spacing size 119 */ 120 public phoneSwitchTextMarginInit(): void { 121 let fontSizeScale = parseFloat(systemParameterEnhance.getSync(CommonConstants.FONT_SIZE_SCALE_PARAM, '1')); 122 this.phoneSwitchTextTopMargin = this.UpdateMarginBasedOnFontSize(17, fontSizeScale); 123 this.phoneSwitchTextBottomMargin = this.UpdateMarginBasedOnFontSize(18, fontSizeScale); 124 } 125 126 /** 127 * @description Calculate the ImageAnimator size 128 * Calculation rule: 129 * 1.general phone & pad : screen height/2 compare with width,shorter is ImageAnimator width 130 * 2.pc: screen height/2 compare with width,shorter is ImageAnimator height 131 * 3.ratio: 3:2 132 * @param height 133 * @param width 134 */ 135 getImageAnimatorSize(height: Length, width: Length) { 136 console.log(TAG + 'getImageAnimatorSize in, deviceInfo.deviceType : ' + deviceInfo.deviceType); 137 let totalHeight = height as number; 138 this.shortSideSize = width < totalHeight / 2 ? width as number : totalHeight / 2 as number; 139 if (deviceInfo.deviceType === 'phone' || deviceInfo.deviceType === 'tablet') { 140 this.imageAnimatorWidth = this.shortSideSize * 0.8; 141 this.imageAnimatorHeight = this.shortSideSize * 0.8 * 2 / 3; 142 } else if (deviceInfo.deviceType === '2in1') { 143 this.imageAnimatorWidth = this.shortSideSize * 0.8 * 3 / 2; 144 this.imageAnimatorHeight = this.shortSideSize * 0.8; 145 } 146 console.log(TAG + 'this.shortSideSize = ' + this.shortSideSize + ', this.imageAnimatorWidth = ' + 147 this.imageAnimatorWidth + ', this.imageAnimatorHeight = ' + this.imageAnimatorHeight); 148 } 149 150 getStringSync() { 151 console.log(TAG + 'getStringSync in'); 152 try { 153 context.resourceManager.getStringValue($r('app.string.pasteboard_title') 154 .id, (error: BusinessError, value: string) => { 155 if (error != null) { 156 console.error(TAG + 'error is ' + error); 157 } else { 158 this.title = value; 159 console.info(TAG + '<aboutToAppear> this.title : ' + this.title); 160 } 161 }) 162 } catch (error) { 163 let code: number = (error as BusinessError).code; 164 let message: string = (error as BusinessError).message; 165 console.error(TAG + `callback getStringValue failed,error code: ${code},message: ${message}.`); 166 } 167 } 168 169 getImageArray() { 170 console.info(TAG + 'getImageArray in'); 171 for (let i = 1; i <= CommonConstants.IMAGE_COUNT; i++) { 172 this.imageArray.push({ 173 src: $r(`app.media.pasteboard_${i}`), 174 duration: (i == CommonConstants.IMAGE_COUNT) ? CommonConstants.IMG_ANIMATOR_OVER_DURATION 175 : CommonConstants.IMG_ANIMATOR_NORMAL_DURATION 176 }) 177 } 178 } 179 180 onPageShow() { 181 console.log(TAG + 'onPageShow in'); 182 this.getGapLength(); 183 display.getAllDisplays((err, data) => { 184 this.screenWidth = px2vp(data[0].width); 185 this.screenHeight = px2vp(data[0].height); 186 this.contentHeight = this.screenHeight; 187 console.log(TAG + 'screenWidth = ' + this.screenWidth + '; screenHeight = ' + this.screenHeight); 188 }) 189 this.is2in1 = deviceInfo.deviceType === '2in1' ? true : false; 190 } 191 192 aboutToAppear() { 193 console.log(TAG + 'aboutToAppear in'); 194 // Switch State Initialization 195 let value: string = ''; 196 try { 197 value = settings.getValueSync(context, 'distributed_pasteboard_switch', switchState.open, 198 settings.domainName.USER_SECURITY); 199 } catch (error) { 200 console.error(`Failed to get setting value:`, error); 201 return; 202 } 203 this.isSwitchOn = value != switchState.close ? true : false; 204 console.log(TAG + '<aboutToAppear> this.isSwitchOn : ' + this.isSwitchOn + '; value: ' + value); 205 206 AppStorage.setOrCreate('isSwitchOn', this.isSwitchOn); 207 console.log(TAG + 'AppStorage.get<boolean>(isSwitchOn) : ' + AppStorage.get<boolean>('isSwitchOn')); 208 209 if (this.isSwitchOn) { 210 let status: boolean = false; 211 try { 212 status = settings.setValueSync(context, 'distributed_pasteboard_switch', switchState.open, 213 settings.domainName.USER_SECURITY); 214 } catch (error) { 215 console.error(`Failed to get setting value:`, error); 216 return; 217 } 218 console.log(TAG + 'set value success :' + status + '; set:distributed_pasteboard_switch is 1'); 219 } 220 221 this.getStringSync(); 222 this.getImageArray(); 223 224 this.listener.on('change', (mediaQueryResult: mediaQuery.MediaQueryResult) => { 225 this.onPortrait(mediaQueryResult); 226 }) 227 this.extContext = localStorage.get<common.UIExtensionContext>('context'); 228 this.startReason = AppStorage.get<string>('startReason'); 229 this.isShowBack = AppStorage.get<boolean>('isShowBack') ?? true; 230 console.info(`${TAG} aboutToAppear: startReason is ${this.startReason}, isShowBack: ${this.isShowBack}`); 231 if (this.isPhone()) { 232 this.checkFoldBackButton(); 233 } 234 this.checkPcPadBackButton(); 235 this.phoneSwitchTextMarginInit(); 236 setTimeout(() => { 237 this.isAnimatorDone = true; 238 }, 20) 239 } 240 241 aboutToDisappear() { 242 console.info(`${TAG} aboutToDisappear in`); 243 } 244 245 getGapLength() { 246 console.log(TAG + 'getGapLength in, deviceInfo.deviceType : ' + deviceInfo.deviceType); 247 if (deviceInfo.deviceType == 'phone') { 248 this.gapLength = CommonConstants.GENERAL_PHONE_GAP_LENGTH; 249 } else if (deviceInfo.deviceType == '2in1' || deviceInfo.deviceType == 'tablet') { 250 this.gapLength = CommonConstants.PC_PAD_GAP_LENGTH; 251 } 252 console.log(TAG + 'this.gapLength : ' + this.gapLength); 253 } 254 255 onBackPress() { 256 console.log(TAG + 'onBackPress in'); 257 } 258 259 @Builder 260 NormalRootContent() { 261 this.titleBar(); 262 this.ContentBuilder(); 263 } 264 265 @Builder 266 SearchRootContent() { 267 NavDestination() { 268 this.ContentBuilder(); 269 } 270 .hideTitleBar(false) 271 .title($r('app.string.pasteboard_title'), { 272 paddingStart: this.is2in1 ? this.paddingStartPc : this.paddingStartNormal, 273 }) 274 .backgroundColor($r('sys.color.ohos_id_color_titlebar_sub_bg')) 275 } 276 277 @Builder 278 titleBar() { 279 Column() { 280 EditableTitleBar({ 281 leftIconStyle: EditableLeftIconType.Back, 282 title: $r('app.string.pasteboard_title'), 283 isSaveIconRequired: false, 284 contentMargin: this.is2in1 ? this.titleBarMarginPc : this.titleBarMarginNormal, 285 onCancel: () => { 286 if (this.isBacked) { 287 console.info(`${TAG} onCancel: The back button has been clicked.`); 288 return; 289 } 290 try { 291 if (this.pasteboardSession && this.pasteboardSession.sendData) { 292 this.isBacked = true; 293 this.pasteboardSession.sendData({ 'action': 'pop' }) 294 } else { 295 console.error(TAG + 'pasteboardSession is undefined'); 296 } 297 } catch (error) { 298 let code: number = (error as BusinessError).code; 299 let message: string = (error as BusinessError).message; 300 console.error(`${TAG} call onCancel failed, error code: ${code}, message: ${message}.`); 301 } 302 } 303 }) 304 } 305 } 306 307 @Builder 308 ContentBuilder() { 309 Scroll(this.scroller) { 310 Column() { 311 ImageAnimator() 312 .images(this.imageArray) 313 .state(this.animationState) 314 .reverse(this.reverse) 315 .fillMode(this.iterations) 316 .iterations(this.iterations) 317 .width(this.imageAnimatorWidth) 318 .height(this.imageAnimatorHeight) 319 .onStart(() => { 320 console.info(TAG + 'ImageAnimator Start'); 321 }) 322 .onFinish(() => { 323 console.info(TAG + 'ImageAnimator Finish'); 324 }) 325 326 Text() { 327 Span(this.pasteboardDesc) 328 .fontFamily('HarmonyHeiTi') 329 .fontSize($r('sys.float.ohos_id_text_size_body2')) 330 .fontColor($r('sys.color.ohos_id_color_text_secondary')) 331 .fontWeight(FontWeight.Regular) 332 Span(this.learnMore) 333 .fontFamily('HarmonyHeiTi') 334 .fontSize($r('sys.float.ohos_id_text_size_body2')) 335 .fontColor($r('sys.color.ohos_id_color_text_primary_activated')) 336 .fontWeight(FontWeight.Medium) 337 .onClick(() => { 338 TipsJumpUtils.jumpTips(getContext(this) as common.UIAbilityContext, CommonConstants.FUN_NUM, 339 CommonConstants.TIPS_TYPE); 340 }) 341 } 342 .margin({ 343 bottom: CommonConstants.PASTEBOARD_DESC_TEXT_MARGIN_BOTTOM, 344 top: CommonConstants.PASTEBOARD_DESC_TEXT_MARGIN_TOP 345 }) 346 .textAlign(TextAlign.Center) 347 .width('100%') 348 349 Column() { 350 Flex({ 351 direction: FlexDirection.Row, 352 justifyContent: FlexAlign.SpaceBetween, 353 alignItems: ItemAlign.Center 354 }) { 355 Text($r('app.string.pasteboard_title')) 356 .fontSize($r('sys.float.ohos_id_text_size_sub_title2')) 357 .fontWeight(FontWeight.Medium) 358 .fontColor($r('sys.color.ohos_id_color_text_primary')) 359 .accessibilityLevel('no') 360 .maxFontScale(2.8) 361 .padding({ 362 top: this.is2in1 ? CommonConstants.ITEM_LIST_PADDING_TOP_PC : this.phoneSwitchTextTopMargin, 363 bottom: this.is2in1 ? CommonConstants.ITEM_LIST_PADDING_BOTTOM_PC : this.phoneSwitchTextBottomMargin 364 }) 365 366 Toggle({ type: ToggleType.Switch, isOn: this.isSwitchOn }) 367 .width(CommonConstants.PASTEBOARD_SWITCH_WIDTH) 368 .height(CommonConstants.PASTEBOARD_SWITCH_HEIGHT) 369 .hoverEffect(HoverEffect.None) 370 .onChange((isOn: boolean) => { 371 console.log(TAG + 'isOn:' + isOn); 372 this.isSwitchOn = isOn; 373 AppStorage.setAndLink('isSwitchOn', isOn); 374 if (isOn) { 375 let status: boolean = false; 376 try { 377 status = settings.setValueSync(context, 'distributed_pasteboard_switch', switchState.open, 378 settings.domainName.USER_SECURITY); 379 } catch (error) { 380 console.error(`Failed to get setting value:`, error); 381 return; 382 } 383 console.log(TAG + 'is set success :' + status + '; set:distributed_pasteboard_switch is on'); 384 } else { 385 let status: boolean = false; 386 try { 387 status = settings.setValueSync(context, 'distributed_pasteboard_switch', switchState.close, 388 settings.domainName.USER_SECURITY); 389 } catch (error) { 390 console.error(`Failed to get setting value:`, error); 391 return; 392 } 393 } 394 }) 395 } 396 .width('100%') 397 .padding({ 398 left: CommonConstants.TEXT_LIST_ALIGN_DISTANCE, 399 right: CommonConstants.TEXT_LIST_ALIGN_DISTANCE 400 }) 401 .backgroundColor($r('sys.color.ohos_id_color_list_card_bg')) 402 .borderRadius(this.is2in1 ? CommonConstants.PC_BORDER_RADIUS : CommonConstants.NON_PC_BORDER_RADIUS) 403 .accessibilityText(this.title) 404 } 405 .width('100%') 406 .constraintSize({ 407 minHeight: CommonConstants.PC_LIST_HEIGHT 408 }) 409 410 Column() { 411 Flex({ 412 direction: FlexDirection.Row, 413 justifyContent: FlexAlign.Start, 414 alignItems: ItemAlign.Center 415 }) { 416 SymbolGlyph($r('sys.symbol.info_circle_fill')) 417 .fontWeight(FontWeight.Medium) 418 .fontSize(CommonConstants.SYMBOL_INFO_CIRCLE) 419 .fontColor([$r('sys.color.ohos_id_color_activated')]) 420 .margin({ right: CommonConstants.SYMBOL_MARGIN_RIGHT }) 421 422 Text($r('app.string.update_version_prompt')) 423 .fontSize($r('sys.float.ohos_id_text_size_body3')) 424 .fontWeight(FontWeight.Medium) 425 .fontColor($r('sys.color.ohos_id_color_text_primary')) 426 .textAlign(TextAlign.Start) 427 .lineHeight(CommonConstants.UPDATE_PROMPT_LINE_HEIGHT) 428 } 429 .margin({ top: CommonConstants.UPDATE_PROMPT_MARGIN_TOP }) 430 } 431 .padding({ 432 left: CommonConstants.TEXT_LIST_ALIGN_DISTANCE, 433 right: CommonConstants.TEXT_LIST_ALIGN_DISTANCE 434 }) 435 } 436 .width('100%') 437 .padding({ left: this.gapLength, right: this.gapLength }) 438 .margin({ bottom: this.contentHeight * 0.09 }) 439 .backgroundColor($r('sys.color.ohos_id_color_sub_background')) 440 } 441 .width('100%') 442 .height(this.screenHeight) 443 .scrollable(ScrollDirection.Vertical) 444 .scrollBar(BarState.Off) 445 .align(Alignment.TopStart) 446 .friction(0.6) 447 .edgeEffect(EdgeEffect.Spring) 448 .onWillScroll((xOffset: number, yOffset: number) => { 449 console.info(TAG + 'onScroll : xOffset:' + xOffset + ' ; yOffset' + yOffset); 450 }) 451 .onScrollEdge(() => { 452 console.info('To the edge'); 453 }) 454 .onScrollStop(() => { 455 console.info('Scroll Stop'); 456 }) 457 .onAreaChange((oldArea: Area, newArea: Area) => { 458 console.log(TAG + 'Scroll, oldArea.height = ' + oldArea.height + ', newArea.height = ' + newArea.height); 459 }) 460 } 461 462 build() { 463 Column() { 464 if (this.isShowBack) { 465 this.NormalRootContent(); 466 } else { 467 this.SearchRootContent(); 468 } 469 } 470 .width('100%') 471 .height('100%') 472 .backgroundColor($r('sys.color.ohos_id_color_sub_background')) 473 .onAreaChange((oldArea: Area, newArea: Area) => { 474 console.log(TAG + 'build column , oldArea.height = ' + oldArea.height + ', newArea.height = ' + newArea.height); 475 this.getImageAnimatorSize(newArea.height, newArea.width); 476 console.info(TAG + 'this.shortSideSize = ' + this.shortSideSize + ', this.imageAnimatorWidth = ' + 477 this.imageAnimatorWidth + ', this.imageAnimatorHeight = ' + this.imageAnimatorHeight); 478 }) 479 } 480 481 private getContentVisibility(): Visibility { 482 return (this.imageAnimatorWidth !== 0 && this.isAnimatorDone) ? Visibility.Visible : Visibility.Hidden; 483 } 484 485 private getImageAnimatorHeight(): number { 486 if (deviceInfo.deviceType === 'phone') { 487 return CommonConstants.ANIMATOR_HEIGHT_PHONE; 488 } else if (deviceInfo.deviceType === '2in1') { 489 return CommonConstants.ANIMATOR_HEIGHT_PC; 490 } else if (deviceInfo.deviceType === 'tablet') { 491 return CommonConstants.ANIMATOR_HEIGHT_PAD; 492 } 493 return CommonConstants.ANIMATOR_HEIGHT_PHONE; 494 } 495 496 private getImageAnimatorWidth(): number { 497 if (deviceInfo.deviceType === 'phone') { 498 return CommonConstants.ANIMATOR_WIDTH_PHONE; 499 } else if (deviceInfo.deviceType === '2in1') { 500 return CommonConstants.ANIMATOR_WIDTH_PC; 501 } else if (deviceInfo.deviceType === 'tablet') { 502 return CommonConstants.ANIMATOR_WIDTH_PAD; 503 } 504 return CommonConstants.ANIMATOR_WIDTH_PHONE; 505 } 506 507 private checkPcPadBackButton(): void { 508 console.info(`${TAG} checkPcPadBackButton in`); 509 if (this.startReason === 'from_search') { 510 if (deviceInfo.deviceType === '2in1' || deviceInfo.deviceType === 'tablet') { 511 this.isShowBack = false; 512 } 513 } 514 } 515 516 private isPhone(): boolean { 517 console.info(`${TAG} isPhone in`); 518 return (deviceInfo.deviceType === 'phone' || deviceInfo.deviceType === 'default'); 519 } 520 521 private refreshFoldStatus(foldStatus: display.FoldStatus): void { 522 console.info(`${TAG} refreshFoldStatus in. foldStatus: ${foldStatus}. startReason: ${this.startReason}`); 523 if (this.startReason === 'from_search') { 524 if (foldStatus === display.FoldStatus.FOLD_STATUS_FOLDED) { 525 this.isShowBack = true; 526 console.info(`${TAG} foldStatus: ${foldStatus}. this.isShowBack: ${this.isShowBack}`); 527 } else { 528 this.isShowBack = false; 529 console.info(`${TAG} foldStatus: ${foldStatus}. this.isShowBack: ${this.isShowBack}`); 530 } 531 } else { 532 this.isShowBack = true; 533 console.info(`${TAG} startReason: ${this.startReason}. this.isShowBack: ${this.isShowBack}`); 534 } 535 } 536 537 private checkFoldBackButton(): void { 538 console.info(`${TAG} checkFoldBackButton in`); 539 try { 540 if (display.isFoldable()) { 541 display.on('foldStatusChange', (foldStatus: display.FoldStatus) => { 542 let foldStatusValue = foldStatus.valueOf(); 543 console.info(`${TAG} checkFoldBackButton: foldStatusValue is ${foldStatusValue}`); 544 this.refreshFoldStatus(foldStatusValue); 545 }) 546 let data: display.FoldStatus = display.getFoldStatus(); 547 this.refreshFoldStatus(data); 548 } 549 } catch (err) { 550 console.error(`${TAG} Register failed. exception: ${err.message}`); 551 } 552 } 553}