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