• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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