• 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, 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