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 { AttributeUpdater } from '@ohos.arkui.modifier'; 16import resourceManager from '@ohos.resourceManager'; 17import { BusinessError } from '@ohos.base'; 18import intl from '@ohos.intl'; 19import componentUtils from '@ohos.arkui.componentUtils'; 20import measure from '@ohos.measure'; 21import promptAction from '@ohos.promptAction'; 22import EnvironmentCallback from '@ohos.app.ability.EnvironmentCallback'; 23import common from '@ohos.app.ability.common'; 24import { Configuration } from '@ohos.app.ability.Configuration'; 25import ConfigurationConstant from '@ohos.app.ability.ConfigurationConstant'; 26import AbilityConstant from '@ohos.app.ability.AbilityConstant'; 27import { ComponentContent, LengthMetrics } from '@ohos.arkui.node'; 28import window from '@ohos.window'; 29import rpc from '@ohos.rpc'; 30import unifiedDataChannel from '@ohos.data.unifiedDataChannel'; 31import uniformDataStruct from '@ohos.data.uniformDataStruct'; 32import uniformTypeDescriptor from '@ohos.data.uniformTypeDescriptor'; 33import CommonConstants, { 34 ProgressPromptText, 35 ProgressErrorPromptText, 36 SignalValue 37} from '../common/constants/CommonConstants'; 38import GlobalContext from '../PasteboardProgressAbility/GlobalContext'; 39 40const TAG = 'PasteboardProgressAbility: '; 41const DIALOG_MARGIN: number = getNumberByResourceId($r('sys.float.padding_level8').id, 16); 42const PERCENTAGE_CALCULATION: number = 100; 43const DIALOG_MAX_WIDTH: number = 400; 44const DIALOG_MAX_HEIGHT_PERCENTAGE: number = 0.9; 45const PROGRESS_SIGNAL_IPC_CODE: number = 0; 46const PROGRESS_SIGNAL_IPC_CODE_IDL: number = 1; 47 48function getNumberByResourceId(resourceId: number, defaultValue: number, allowZero?: boolean): number { 49 try { 50 let sourceValue: number = resourceManager.getSystemResourceManager().getNumber(resourceId); 51 if (sourceValue > 0 || allowZero) { 52 return sourceValue; 53 } else { 54 return defaultValue; 55 } 56 } catch (error) { 57 console.error(TAG + `GetNumberByResourceId error, code: ${error.code}, message: ${error.message}`); 58 return defaultValue; 59 } 60} 61 62class TextModifier extends AttributeUpdater<TextAttribute, TextInterface> { 63 initializeModifier(instance: TextAttribute): void { 64 } 65} 66 67class ProgressModifier extends AttributeUpdater<ProgressAttribute, ProgressInterface> { 68 initializeModifier(instance: ProgressAttribute): void { 69 } 70} 71 72class WantInfo { 73 public parameters: Object[] 74 75 constructor(parameters: Object[]) { 76 this.parameters = parameters 77 } 78} 79 80class Params { 81 public currentNum: number = 0; 82 public promptText: Resource; 83 public winCloseCallback: Callback<boolean>; 84 public updateConfiguration: Callback<void>; 85 86 constructor(currentNum: number, promptText: Resource, winCloseCallback: Callback<boolean>, 87 updateConfiguration: Callback<void>) { 88 this.currentNum = currentNum; 89 this.promptText = promptText; 90 this.winCloseCallback = winCloseCallback; 91 this.updateConfiguration = updateConfiguration; 92 } 93} 94 95let storage = LocalStorage.getShared(); 96 97@Entry(storage) 98@Component 99export struct Index { 100 @LocalStorageLink('want') want: WantInfo = new WantInfo([]); 101 @LocalStorageLink('win') win: window.Window = {} as window.Window; 102 @LocalStorageLink('globalContext') globalContext: GlobalContext = new GlobalContext(); 103 @LocalStorageLink('serviceContext') serviceContext: common.ServiceExtensionContext | undefined = undefined; 104 private ctx: UIContext | undefined = this.getUIContext(); 105 private params: Params | undefined = undefined; 106 private contentNode: ComponentContent<Params> | undefined = undefined; 107 private timer: number | null = null; 108 private pollInterval: number = 50; 109 private timeoutInterval: number = 10000; 110 private unChangeProgressTimes: number = 0; 111 private isTimeoutError: boolean = false; 112 private latestUpdateTime: number = 0; 113 114 aboutToAppear(): void { 115 const promptTextPre: string = this.want.parameters['promptText']; 116 const promptText: Resource = this.want.parameters['isRemote'] ? 117 $r(`app.string.${ProgressPromptText[promptTextPre]}`, this.want.parameters['remoteDeviceName']) : 118 $r(`app.string.${ProgressPromptText[promptTextPre]}`); 119 this.params = 120 new Params(0, promptText, (isNeedCancelTask: boolean) => { 121 this.closeDialog(isNeedCancelTask) 122 }, () => this.updateConfiguration()); 123 this.contentNode = new ComponentContent<Params>(this.ctx as UIContext, wrapBuilder(buildProgress), this.params); 124 let options: promptAction.BaseDialogOptions = { 125 autoCancel: false, 126 onWillDismiss: (dismissDialogAction: DismissDialogAction) => { 127 console.info(TAG + `reason: ${JSON.stringify(dismissDialogAction.reason)}`); 128 if (dismissDialogAction.reason === DismissReason.PRESS_BACK) { 129 return; 130 } 131 dismissDialogAction.dismiss(); 132 }, 133 onDidDisappear: () => { 134 this.onDialogDisappear(); 135 } 136 }; 137 this.getUIContext() 138 .getPromptAction() 139 .openCustomDialog(this.contentNode, options) 140 .then(() => { 141 this.queryProgressData(); 142 }) 143 .catch((error: BusinessError) => { 144 console.error(TAG + `OpenCustomDialog args error code is ${error.code}, message is ${error.message}`); 145 }) 146 } 147 148 queryProgressData() { 149 let options: unifiedDataChannel.Options = { 150 key: this.want.parameters['progressKey'] 151 }; 152 try { 153 unifiedDataChannel.queryData(options, (err, data) => { 154 if (err || !data[0]?.getRecords()?.length) { 155 this.unChangeProgressTimes++; 156 if (err) { 157 console.error(TAG + `Failed to query data. code is ${err.code}, message is ${err.message}`); 158 } 159 if (this.unChangeProgressTimes > this.timeoutInterval / this.pollInterval) { 160 this.handleTimeoutError(); 161 } else { 162 this.pollQuery(); 163 } 164 return; 165 } 166 let records = data[0].getRecords(); 167 let record = records[0].getValue() as uniformDataStruct.PlainText; 168 console.info(TAG + `Succeeded in querying data. record.textContent = ${record.textContent}`); 169 const currentNum = Number(record.textContent) || 0; 170 const updateTime = Number(record.abstract) || 0; 171 if ((this.params as Params).currentNum === currentNum && this.latestUpdateTime == updateTime) { 172 this.unChangeProgressTimes++; 173 } else { 174 this.unChangeProgressTimes = 0; 175 } 176 if (this.unChangeProgressTimes > this.timeoutInterval / this.pollInterval) { 177 this.handleTimeoutError(); 178 } else { 179 this.latestUpdateTime = updateTime; 180 (this.params as Params).currentNum = currentNum; 181 this.contentNode?.update(this.params); 182 if (currentNum < PERCENTAGE_CALCULATION) { 183 this.pollQuery(); 184 } 185 } 186 }) 187 } catch (error) { 188 console.error(TAG + `Query data throws an exception. code is ${error.code}, message is ${error.message}`); 189 } 190 } 191 192 private pollQuery(): void { 193 this.timer = setTimeout(() => { 194 this.queryProgressData(); 195 }, this.pollInterval); 196 } 197 198 private handleTimeoutError(): void { 199 this.isTimeoutError = true; 200 const promptTextPre: string = this.want.parameters['promptText']; 201 (this.params as Params).promptText = $r(`app.string.${ProgressErrorPromptText[promptTextPre]}`); 202 this.contentNode?.update(this.params); 203 console.error(TAG + `unChangeProgressTimes is ${this.unChangeProgressTimes}`); 204 } 205 206 private onDialogDisappear(): void { 207 console.info(TAG + 'on disappear'); 208 clearTimeout(this.timer); 209 this.ctx = undefined; 210 this.contentNode = undefined; 211 this.params = undefined; 212 } 213 214 closeDialog(isNeedCancelTask: boolean): void { 215 console.info(TAG + 'closeDialog'); 216 if (this.contentNode === undefined) { 217 console.info(TAG + 'already closed'); 218 return; 219 } 220 let promptAction = (this.ctx as UIContext).getPromptAction(); 221 if (!promptAction) { 222 console.error(TAG + 'getPromptAction or closeDialog is null.'); 223 return; 224 } 225 promptAction.closeCustomDialog(this.contentNode); 226 this.contentNode.dispose(); 227 this.win.destroyWindow(); 228 this.globalContext.dialogSet.delete(this.want.parameters?.['progressKey']); 229 console.info(TAG + `dialogSet size is ${this.globalContext.dialogSet.size}`); 230 if (this.globalContext.dialogSet.size === 0) { 231 this.serviceContext?.terminateSelf(); 232 } 233 if (isNeedCancelTask) { 234 this.cancelTask(); 235 this.cancelTaskIdl(); 236 } 237 } 238 239 async cancelTask() { 240 const option = new rpc.MessageOption(); 241 const data = new rpc.MessageSequence(); 242 const reply = new rpc.MessageSequence(); 243 try { 244 const proxy = this.want.parameters['ipcCallback'].value as rpc.IRemoteObject; 245 await data.writeInterfaceToken(CommonConstants.COMP_DIALOG_CALLBACK); 246 await data.writeString(this.isTimeoutError ? SignalValue.CANCEL_TIMEOUT_ERROR : SignalValue.CANCEL_PROGRESS_TASK); 247 proxy.sendMessageRequest(PROGRESS_SIGNAL_IPC_CODE, data, reply, option); 248 } catch (err) { 249 console.error(TAG + `hap sendMessageRequest failed: ${JSON.stringify(err)}`); 250 } finally { 251 data.reclaim(); 252 reply.reclaim(); 253 } 254 } 255 256 async cancelTaskIdl() { 257 const option = new rpc.MessageOption(); 258 const data = new rpc.MessageSequence(); 259 const reply = new rpc.MessageSequence(); 260 try { 261 const proxy = this.want.parameters['ipcCallback'].value as rpc.IRemoteObject; 262 await data.writeInterfaceToken(CommonConstants.COMP_DIALOG_CALLBACK_IDL); 263 await data.writeString(this.isTimeoutError ? SignalValue.CANCEL_TIMEOUT_ERROR : SignalValue.CANCEL_PROGRESS_TASK); 264 proxy.sendMessageRequest(PROGRESS_SIGNAL_IPC_CODE_IDL, data, reply, option); 265 } catch (err) { 266 console.error(TAG + `hap sendMessageRequest failed: ${JSON.stringify(err)}`); 267 } finally { 268 data.reclaim(); 269 reply.reclaim(); 270 } 271 } 272 273 updateConfiguration(): void { 274 if (this.contentNode === undefined) { 275 console.info(TAG + 'already closed'); 276 return; 277 } 278 this.contentNode?.updateConfiguration(); 279 } 280 281 build() { 282 } 283} 284 285@Builder 286function buildProgress($$: Params) { 287 PasteboardProgress({ 288 currentNum: $$.currentNum, 289 promptText: $$.promptText, 290 closeCallback: $$.winCloseCallback, 291 updateConfiguration: $$.updateConfiguration, 292 }) 293} 294 295@Component 296struct PasteboardProgress { 297 @State winWidth: number = 0; 298 @State maxHeight: number = 0; 299 @State oneLineOverFlow: boolean = false; 300 private promptTextMarginRight: number = 17; 301 @Prop @Watch('currentPercentageChange') currentNum: number = 0; 302 @Prop @Watch('onPromptTextChange') promptText: Resource; 303 @State @Watch('onIdleWidthChanged') idleWidth: number = 100; 304 public closeCallback: Callback<boolean> = () => { 305 }; 306 public updateConfiguration: Callback<void> = () => { 307 }; 308 private progressModifier: ProgressModifier = new ProgressModifier(); 309 private percentageTextModifier: TextModifier = new TextModifier(); 310 private numberFormat: intl.NumberFormat = new intl.NumberFormat('', { style: 'percent' }); 311 private currentPercentage: string = this.numberFormat.format(0); 312 private currentPercentageNum: number = 0; 313 private callbackId: number = 0; 314 private context = getContext(this) as common.UIAbilityContext; 315 private systemLanguage: string | undefined = this.context.config.language; 316 private systemColorMode: ConfigurationConstant.ColorMode | undefined = this.context.config.colorMode; 317 318 async aboutToAppear(): Promise<void> { 319 console.info(TAG + 'PasteboardProgress aboutToAppear'); 320 let applicationContext: common.ApplicationContext = this.context.getApplicationContext(); 321 let configurationChange = (newConfig: Configuration) => this.onConfigurationUpdatedCall(newConfig); 322 323 let environmentCallback: EnvironmentCallback = { 324 onConfigurationUpdated(newConfig: Configuration) { 325 configurationChange(newConfig); 326 }, 327 onMemoryLevel(level: AbilityConstant.MemoryLevel) { 328 console.info(TAG + `onMemoryLevel level: ${level}`); 329 } 330 } 331 332 try { 333 this.callbackId = applicationContext.on('environment', environmentCallback); 334 } catch (err) { 335 console.error(TAG + `Failed to register applicationContext. Code is ${err.code}, message is ${err.message}`); 336 } 337 } 338 339 onConfigurationUpdatedCall(newConfig: Configuration): void { 340 console.info(TAG + `onConfigurationUpdated newConfig: ${JSON.stringify(newConfig)}`); 341 if (this.systemLanguage !== newConfig.language) { 342 console.info(TAG + `systemLanguage from ${this.systemLanguage} changed to ${newConfig.language}`); 343 this.systemLanguage = newConfig.language; 344 } 345 if (this.systemColorMode !== newConfig.colorMode) { 346 console.info(TAG + `systemColorMode from ${this.systemColorMode} changed to ${newConfig.colorMode}`); 347 this.systemColorMode = newConfig.colorMode; 348 } 349 this.updateConfiguration(); 350 } 351 352 aboutToDisappear(): void { 353 console.info(TAG + 'aboutToDisappear applicationContext off environment.'); 354 let applicationContext: common.ApplicationContext = this.context.getApplicationContext(); 355 try { 356 applicationContext.off('environment', this.callbackId); 357 this.callbackId = 0; 358 } catch (err) { 359 console.error(TAG + `Failed to unregister applicationContext. Code is ${err.code}, message is ${err.message}`); 360 } 361 } 362 363 currentPercentageChange(): void { 364 if (this.currentNum <= 0) { 365 this.currentPercentageNum = 0; 366 this.currentPercentage = ''; 367 } else if (this.currentPercentageNum > this.currentNum || this.currentNum > 100) { 368 console.info(TAG + 369 `current num exception. currentPercentageNum: ${this.currentPercentageNum} currentNum: ${this.currentNum}`); 370 return; 371 } 372 console.info(TAG + `current num currentNum. ${this.currentNum}`); 373 this.currentPercentageNum = Math.round(this.currentNum); 374 this.currentPercentage = this.numberFormat.format(this.currentNum / PERCENTAGE_CALCULATION); 375 this.percentageTextModifier.updateConstructorParams(this.currentPercentage); 376 this.progressModifier.attribute?.value(this.currentPercentageNum); 377 this.progressModifier.updateConstructorParams({ value: this.currentPercentageNum }); 378 if (this.currentPercentageNum === PERCENTAGE_CALCULATION) { 379 setTimeout(() => this.closeCallback(false), 300); 380 } 381 } 382 383 onPromptTextChange() { 384 this.onIdleWidthChanged(); 385 } 386 387 onIdleWidthChanged() { 388 let itemLenWidthPX1: SizeOptions = measure.measureTextSize({ 389 textContent: this.promptText, 390 fontSize: 14, 391 textAlign: TextAlign.Start, 392 fontWeight: FontWeight.Regular 393 }); 394 let itemLenWidthPX2: SizeOptions = measure.measureTextSize({ 395 textContent: this.currentPercentage, 396 fontSize: 14, 397 fontWeight: FontWeight.Regular 398 }) 399 if (this.idleWidth > 400 px2vp((itemLenWidthPX1.width as number) + (itemLenWidthPX2.width as number)) + this.promptTextMarginRight) { 401 this.oneLineOverFlow = false 402 } else { 403 this.oneLineOverFlow = true 404 } 405 } 406 407 build() { 408 RelativeContainer() { 409 Row() { 410 this.pastedProgressLoading() 411 } 412 .width(this.winWidth) 413 .constraintSize({ maxWidth: DIALOG_MAX_WIDTH, maxHeight: this.maxHeight }) 414 .margin({ 415 left: $r('sys.float.padding_level8'), 416 right: $r('sys.float.padding_level8') 417 }) 418 .borderRadius($r('sys.float.ohos_id_corner_radius_panel')) 419 .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK) 420 .alignRules({ 421 center: { anchor: '__container__', align: VerticalAlign.Center }, 422 middle: { anchor: '__container__', align: HorizontalAlign.Center } 423 }) 424 } 425 .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => { 426 console.info(TAG + 'dialog size change. width: ' + newValue.width + ' height: ' + newValue.height); 427 if (Number(newValue.width) === 0 || Number(newValue.height) === 0) { 428 return; 429 } 430 this.winWidth = Number(newValue.width) - DIALOG_MARGIN * 2; 431 this.maxHeight = Number(newValue.height) * DIALOG_MAX_HEIGHT_PERCENTAGE - 28; 432 }) 433 .height('100%') 434 .width('100%') 435 } 436 437 @Builder 438 pastedProgressLoading() { 439 if (this.oneLineOverFlow) { 440 Column() { 441 this.showPromptText(2, 2, undefined) 442 this.BuilderPercentageText(2) 443 Row() { 444 this.showProgress(1) 445 this.cancelButton() 446 } 447 } 448 .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => { 449 this.idleWidth = 450 newValue.width as number - px2vp(componentUtils.getRectangleById('close_button').size.width) - 24 451 }) 452 .alignItems(HorizontalAlign.Start) 453 .margin({ 454 left: 24, 455 right: 24, 456 top: 33.5, 457 bottom: 24.5 458 }) 459 } else { 460 Row() { 461 Column() { 462 Row() { 463 this.showPromptText(1, undefined, 1) 464 this.BuilderPercentageText(2) 465 } 466 .margin({ left: 3, bottom: 4.5 }) 467 .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => { 468 this.idleWidth = newValue.width as number 469 }) 470 .justifyContent(FlexAlign.Start) 471 472 this.showProgress(undefined) 473 } 474 .alignItems(HorizontalAlign.Start) 475 .layoutWeight(1) 476 477 this.cancelButton() 478 } 479 .alignItems(VerticalAlign.Bottom) 480 .margin({ 481 left: 24, 482 right: 24, 483 top: 33.5, 484 bottom: 24.5 485 }) 486 } 487 } 488 489 @Builder 490 showPromptText(maxLines: number, maxFontScale: number | undefined, layoutWeight: number | undefined) { 491 Text(this.promptText) 492 .fontSize(14) 493 .fontFamily('HarmonyHeiTi') 494 .fontWeight(FontWeight.Regular) 495 .fontColor($r('sys.color.font_primary')) 496 .constraintSize({ minHeight: 20 }) 497 .wordBreak(WordBreak.BREAK_ALL) 498 .textAlign(TextAlign.Start) 499 .textOverflow({ overflow: TextOverflow.Ellipsis }) 500 .margin({ end: LengthMetrics.vp(this.promptTextMarginRight) }) 501 .maxLines(maxLines) 502 .maxFontScale(maxFontScale) 503 .layoutWeight(layoutWeight) 504 } 505 506 @Builder 507 BuilderPercentageText(maxFontScale: number | undefined) { 508 Text(this.currentPercentage) 509 .attributeModifier(this.percentageTextModifier) 510 .fontSize(14) 511 .fontWeight(FontWeight.Regular) 512 .fontFamily('HarmonyHeiTi') 513 .fontColor($r('sys.color.font_secondary')) 514 .lineHeight(19) 515 .maxLines(1) 516 .constraintSize({ minHeight: 20 }) 517 .wordBreak(WordBreak.BREAK_ALL) 518 .maxFontScale(maxFontScale) 519 .onSizeChange(() => { 520 this.onIdleWidthChanged() 521 }) 522 } 523 524 @Builder 525 showProgress(layoutWeight: number | undefined) { 526 Progress({ value: this.currentPercentageNum, type: ProgressType.Linear }) 527 .attributeModifier(this.progressModifier) 528 .color($r('sys.color.comp_background_emphasize')) 529 .height(24) 530 .value(this.currentPercentageNum) 531 .style({ enableSmoothEffect: true }) 532 .layoutWeight(layoutWeight) 533 } 534 535 @Builder 536 cancelButton() { 537 Button({ buttonStyle: ButtonStyleMode.TEXTUAL }) { 538 SymbolGlyph($r('sys.symbol.xmark_circle_fill')) 539 .fontColor([$r('sys.color.ohos_id_color_foreground')]) 540 .fontSize('24vp') 541 } 542 .id('close_button') 543 .opacity(0.4) 544 .margin({ start: LengthMetrics.vp(16) }) 545 .size({ width: 24, height: 24 }) 546 .accessibilityText($r('app.string.progress_cancel_btn')) 547 .onClick(() => { 548 console.warn(TAG + 'pastedProgressLoading on click'); 549 this.closeCallback(true); 550 }) 551 } 552} 553