1/**
2 * Copyright (c) 2023-2023 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 */
15
16import bundleMonitor from '@ohos.bundle.bundleMonitor';
17import router from '@ohos.router';
18import deviceInfo from '@ohos.deviceInfo';
19import accessibility from '@ohos.accessibility';
20import config from '@ohos.accessibility.config';
21import { ResourceUtils } from '../model/accessibilityImpl/resourceUtils';
22import LogUtil from '../../../../../../common/utils/src/main/ets/default/baseUtil/LogUtil';
23import ConfigData from '../../../../../../common/utils/src/main/ets/default/baseUtil/ConfigData';
24import LongHeadComponent from '../../../../../../common/component/src/main/ets/default/longHeadComponent';
25import ComponentConfig from '../../../../../../common/component/src/main/ets/default/ComponentConfig';
26
27const MODULE_TAG = ConfigData.TAG + 'service-> ';
28const DEVICE_TYPE = deviceInfo.deviceType;
29
30class ServiceInfoData {
31  serviceIcon: string = '';
32  serviceTitle: string = '';
33  serviceArrow: string = '';
34  serviceSummary: string = '';
35  serviceEndText: string = '';
36  serviceState: boolean = false;
37  serviceId: string = '';
38  serviceBundleName: string = '';
39  serviceUri: string = '';
40}
41
42/**
43 * AccessibilityService Info
44 */
45@Entry
46@Component
47struct AccessibilityServiceInfo {
48  tag: string = 'AccessibilityServiceInfoSettings';
49  isPhoneOrRK: boolean = true;
50  bundleName: string = '';
51  serviceName: string = '';
52  capabilities: Array<accessibility.Capability> = ['retrieve', 'keyEventObserver', 'gesture', 'zoom', 'touchGuide'];
53  private scroller: Scroller = new Scroller();
54  @State serviceIsOn: boolean = false;
55  @State pageChanged: boolean = false;
56  @State serviceIsOnClick: boolean = false;
57  @StorageLink('accessibilityServiceInfo') serviceInfo: ServiceInfoData = {
58    serviceIcon: '',
59    serviceTitle: '',
60    serviceArrow: '',
61    serviceSummary: '',
62    serviceEndText: '',
63    serviceState: false,
64    serviceId: '',
65    serviceBundleName: '',
66    serviceUri: '',
67  };
68  showDialog: CustomDialogController | null = new CustomDialogController({
69    builder: ShowDialog({
70      firstClickAction: () => {
71        this.openServiceButton(this.serviceInfo.serviceId);
72      },
73      secondClickAction: () => {
74        this.cancelButton();
75      },
76    }),
77    alignment: this.isPhoneOrRK ? DialogAlignment.Bottom : DialogAlignment.Center,
78    autoCancel: true,
79    offset: ({ dx: 0, dy: this.isPhoneOrRK ? '-12dp' : 0 }),
80    cancel: this.cancelButton,
81  });
82
83  build() {
84    Column() {
85      GridContainer({ gutter: ConfigData.GRID_CONTAINER_GUTTER_24, margin: ConfigData.GRID_CONTAINER_MARGIN_24 }) {
86        Column() {
87          LongHeadComponent({ longHeadName: this.serviceInfo.serviceTitle, isActive: true });
88          Scroll(this.scroller) {
89            Column() {
90              Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
91                Row() {
92                  Text(this.serviceInfo.serviceTitle)
93                    .fontSize($r('sys.float.ohos_id_text_size_body1'))
94                    .fontColor($r('sys.color.ohos_id_color_text_primary'))
95                    .fontWeight(FontWeight.Medium)
96                    .maxLines(ComponentConfig.MAX_LINES_3)
97                    .textOverflow({ overflow: TextOverflow.Ellipsis })
98                    .width('90%')
99                    .textAlign(TextAlign.Start);
100
101                  Blank();
102
103                  if (this.pageChanged) {
104                    Toggle({ type: ToggleType.Switch, isOn: this.serviceIsOn })
105                      .width('36vp')
106                      .height('20vp')
107                      .selectedColor('#007DFF')
108                      .onClick(() => {
109                        this.serviceIsOnClick = true;
110                      })
111                      .onChange((isOn: boolean) => {
112                        this.setExtensionServiceState(isOn);
113                      });
114                  } else {
115                    Toggle({ type: ToggleType.Switch, isOn: this.serviceIsOn })
116                      .width('36vp')
117                      .height('20vp')
118                      .selectedColor('#007DFF')
119                      .onClick(() => {
120                        this.serviceIsOnClick = true;
121                      })
122                      .onChange((isOn: boolean) => {
123                        this.setExtensionServiceState(isOn);
124                      });
125                  }
126                }
127                .height($r('app.float.wh_value_56'))
128                .width(ConfigData.WH_100_100)
129                .alignItems(VerticalAlign.Center)
130                .padding({ left: $r('app.float.wh_value_12'), right: $r('app.float.wh_value_6') })
131                .backgroundColor($r('app.color.white_bg_color'))
132                .borderRadius($r('app.float.radius_24'));
133              }
134
135              Blank(4);
136
137              Row() {
138                Text($r('app.string.more_settings'))
139                  .fontSize($r('sys.float.ohos_id_text_size_body1'))
140                  .fontColor($r('sys.color.ohos_id_color_text_primary'))
141                  .fontWeight(FontWeight.Medium)
142                  .textAlign(TextAlign.Start);
143
144                Blank();
145                Image('/res/image/ic_settings_arrow.svg')
146                  .width($r('app.float.wh_value_12'))
147                  .height($r('app.float.wh_value_24'))
148                  .margin({ right: $r('app.float.distance_8') })
149                  .fillColor($r('sys.color.ohos_id_color_primary'))
150                  .opacity($r('sys.float.ohos_fa_alpha_content_fourth'));
151              }
152              .height($r('app.float.wh_value_56'))
153              .width(ConfigData.WH_100_100)
154              .alignItems(VerticalAlign.Center)
155              .padding({ left: $r('app.float.wh_value_12'), right: $r('app.float.wh_value_6') })
156              .enabled(false)
157              .backgroundColor($r('app.color.white_bg_color'))
158              .borderRadius($r('app.float.radius_24'));
159
160              Blank(20);
161
162              Text($r('app.string.help'))
163                .width(ConfigData.WH_100_100)
164                .fontSize($r('app.float.font_14'))
165                .fontColor($r('sys.color.ohos_id_color_text_secondary'))
166                .height($r('app.float.wh_value_52'))
167                .lineHeight($r('app.float.wh_value_20'))
168                .padding({
169                  left: $r('app.float.wh_value_12'),
170                  top: $r('app.float.distance_4'),
171                  bottom: $r('app.float.distance_24'),
172                });
173              Text(this.serviceInfo.serviceSummary)
174                .width(ConfigData.WH_100_100)
175                .fontSize($r('app.float.font_14'))
176                .fontColor($r('sys.color.ohos_id_color_text_secondary'))
177                .height('auto')
178                .lineHeight($r('app.float.wh_value_20'))
179                .padding({
180                  left: $r('app.float.wh_value_12'),
181                  top: $r('app.float.distance_4'),
182                  bottom: $r('app.float.distance_80'),
183                });
184            }
185            .useSizeType({
186              sm: { span: 4, offset: 0 },
187              md: { span: 6, offset: 1 },
188              lg: { span: 8, offset: 2 },
189            });
190          }
191          .width(ConfigData.WH_100_100)
192          .height(ConfigData.WH_100_100)
193          .align(Alignment.TopStart)
194          .scrollBar(BarState.Auto)
195          .edgeEffect(EdgeEffect.Spring);
196        }
197      }
198      .width(ConfigData.WH_100_100)
199      .height(ConfigData.WH_100_100);
200    }
201    .backgroundColor($r("sys.color.ohos_id_color_sub_background"))
202    .width(ConfigData.WH_100_100)
203    .height(ConfigData.WH_100_100);
204  }
205
206  aboutToAppear(): void {
207    LogUtil.info(`${this.tag} aboutToAppear start`);
208    if (DEVICE_TYPE === 'phone' || DEVICE_TYPE === 'default') {
209      this.isPhoneOrRK = true;
210    } else {
211      this.isPhoneOrRK = false;
212    }
213    this.openExtensionServiceManagementDetailsStatusListener();
214    this.getServiceName();
215    LogUtil.info(`${this.tag} aboutToAppear end `);
216  }
217
218  aboutToDisappear(): void {
219    LogUtil.info(`${this.tag} aboutToDisappear start`);
220    this.closeExtensionServiceManagementDetailsStatusListener();
221    this.showDialog = null;
222  }
223
224  onPageShow(): void {
225    this.openExtensionServiceManagementDetailsApplicationListener();
226    this.setExtensionServiceManagementApplicationData();
227  }
228
229  getServiceName(): void {
230    let param = router.getParams();
231    if (param) {
232      LogUtil.info(`${this.tag} param is valid`);
233      this.serviceInfo = param as ServiceInfoData;
234      this.bundleName = this.serviceInfo.serviceBundleName;
235      this.serviceName = this.serviceInfo.serviceId;
236      this.serviceIsOn = this.serviceInfo.serviceState;
237      AppStorage.SetOrCreate('ServiceInformation', param);
238    }
239  }
240
241  setExtensionServiceState(isOn: boolean): void {
242    LogUtil.info(`${this.tag} setExtensionServiceState ${isOn}`);
243    LogUtil.info(`${this.tag} serviceState: ${JSON.stringify(this.serviceIsOn)}`);
244    if (isOn) {
245      if (this.serviceIsOnClick) {
246        this.showDialog?.open();
247        this.serviceIsOnClick = false;
248      }
249    } else {
250      this.disableAbility(this.serviceInfo.serviceId);
251    }
252  }
253
254  enableAbility(abilityName: string): void {
255    config.enableAbility(abilityName, this.capabilities).then(() => {
256      LogUtil.info(`${this.tag} enable accessibilityService success`);
257    }).catch((err: object) => {
258      this.serviceIsOn = false;
259      this.pageChanged = !this.pageChanged;
260      LogUtil.error(`${this.tag} failed to enable accessibilityService, because ${JSON.stringify(err)}`);
261    })
262  }
263
264  disableAbility(abilityName: string): void {
265    config.disableAbility(abilityName).then(() => {
266      LogUtil.info(`${MODULE_TAG} disable accessibilityService success`);
267    }).catch((err: object) => {
268      LogUtil.error(`${MODULE_TAG} disable accessibilityService failed, error: ${JSON.stringify(err)}`);
269    });
270  }
271
272  async openServiceButton(abilityName: string): Promise<void> {
273    let enabledServiceList: Array<accessibility.AccessibilityAbilityInfo> = await accessibility.getAccessibilityExtensionList('all', 'enable')
274    if (enabledServiceList && enabledServiceList.length > 0) {
275      for (let enabledService of enabledServiceList) {
276        if (enabledService.id && enabledService.id === abilityName) {
277          this.serviceIsOn = true;
278          return;
279        }
280      }
281    }
282    this.enableAbility(abilityName)
283  }
284
285  cancelButton(): void {
286    this.disableAbility(this.serviceInfo.serviceId);
287    this.serviceIsOn = false;
288    this.pageChanged = !this.pageChanged;
289    LogUtil.info(`${MODULE_TAG} cancel: serviceStatus ${JSON.stringify(this.serviceIsOn)}`);
290  }
291
292  async setExtensionServiceManagementStatusData(): Promise<void> {
293    this.serviceIsOn = false;
294    let enabledServiceList: Array<accessibility.AccessibilityAbilityInfo> = await accessibility.getAccessibilityExtensionList('all', 'enable');
295    if (!enabledServiceList || enabledServiceList.length < 1) {
296      return;
297    }
298    for (let enabledService of enabledServiceList) {
299      if (enabledService.id && enabledService.id === this.serviceInfo.serviceId) {
300        this.serviceIsOn = true;
301        break;
302      }
303    }
304  }
305
306  async setExtensionServiceManagementApplicationData(): Promise<void> {
307    let installServiceList: Array<accessibility.AccessibilityAbilityInfo> = await accessibility.getAccessibilityExtensionList('all', 'install');
308    if (!installServiceList || installServiceList.length < 1) {
309      router.back();
310      return;
311    }
312    let flag: boolean = true;
313    for (let enabledService of installServiceList) {
314      if (enabledService.id && this.serviceInfo.serviceId === enabledService.id) {
315        flag = false;
316        break;
317      }
318    }
319    if (flag) {
320      router.back();
321    }
322  }
323
324  openExtensionServiceManagementDetailsStatusListener(): void {
325    try {
326      config.on('enabledAccessibilityExtensionListChange', () => {
327        LogUtil.info(`subscribe enabled accessibility extension list change state success`);
328        this.setExtensionServiceManagementStatusData();
329      });
330    } catch (exception) {
331      LogUtil.info(`failed to subscribe enabled accessibility extension list change state, because ${JSON.stringify(exception)}`);
332    }
333  }
334
335  closeExtensionServiceManagementDetailsStatusListener(): void {
336    try {
337      config.off('enabledAccessibilityExtensionListChange', () => {
338        LogUtil.info(`Unsubscribe enabled accessibility extension list change state success`);
339      });
340    } catch (exception) {
341      LogUtil.info(`failed to Unsubscribe enabled accessibility extension list change state, because ${JSON.stringify(exception)}`);
342    }
343  }
344
345  openExtensionServiceManagementDetailsApplicationListener(): void {
346    try {
347      bundleMonitor.on('update', (bundleChangeInfo) => {
348        LogUtil.info(`${this.tag} Update bundleName: ${bundleChangeInfo.bundleName} userId: ${bundleChangeInfo.userId}`);
349        LogUtil.info(`${this.tag} Update localBundleName: ${this.bundleName} `);
350        if (this.bundleName === bundleChangeInfo.bundleName) {
351          router.back();
352        }
353
354      });
355    } catch (exception) {
356      LogUtil.info(`${this.tag} failed subscribe bundleMonitor, result: ${JSON.stringify(exception)}`);
357    }
358    try {
359      bundleMonitor.on('remove', (bundleChangeInfo) => {
360        LogUtil.info(`${this.tag} Remove bundleName: ${bundleChangeInfo.bundleName} userId: ${bundleChangeInfo.userId}`);
361        this.setExtensionServiceManagementApplicationData();
362      });
363    } catch (exception) {
364      LogUtil.info(`${this.tag} failed subscribe bundleMonitor, result: ${JSON.stringify(exception)}`);
365    }
366  }
367
368  closeExtensionServiceManagementDetailsApplicationListener(): void {
369    try {
370      bundleMonitor.off('update');
371    } catch (exception) {
372      LogUtil.info(`${this.tag} failed subscribe bundleMonitor, result: ${JSON.stringify(exception)}`);
373    }
374    try {
375      bundleMonitor.off('remove');
376    } catch (exception) {
377      LogUtil.info(`${this.tag} failed subscribe bundleMonitor, result: ${JSON.stringify(exception)}`);
378    }
379  }
380
381  onPageHide(): void {
382    LogUtil.info(`${this.tag} onPageHide start`);
383    this.closeExtensionServiceManagementDetailsApplicationListener();
384  }
385}
386
387/**
388 * Show dialog
389 */
390@CustomDialog
391struct ShowDialog {
392  dialogController?: CustomDialogController;
393  firstClickAction?: () => void;
394  secondClickAction?: () => void;
395  @State @Watch("freezeView")
396  private countDownTime: number = 10000;
397  @State freezingTimeForView: number = -1;
398  @State userApproval: boolean = false;
399  @State title: string = ResourceUtils.getPluralStringValueSync($r('app.plural.user_approval_remain_times'), 10);
400  @State enableState: boolean = false;
401
402  build() {
403    Column() {
404
405      Blank(2);
406
407      Text($r('app.string.accessibility_security_notification'))
408        .width(ConfigData.WH_100_100)
409        .fontSize($r('app.float.font_24'))
410        .fontColor($r('sys.color.ohos_id_color_text_secondary'))
411        .padding({
412          left: $r('app.float.wh_value_12'),
413          top: $r('app.float.distance_4'),
414          bottom: $r('app.float.distance_24'),
415        });
416
417      Text($r('app.string.accessibility_notify_content'))
418        .width(ConfigData.WH_100_100)
419        .fontSize($r('app.float.font_18'))
420        .fontColor($r('sys.color.ohos_id_color_text_secondary'))
421        .padding({
422          left: $r('app.float.wh_value_12'),
423          top: $r('app.float.distance_4'),
424          bottom: $r('app.float.distance_24'),
425        });
426
427      Text($r('app.string.accessibility_risk_detail'))
428        .width(ConfigData.WH_100_100)
429        .fontSize($r('app.float.font_20'))
430        .fontColor($r('sys.color.ohos_id_color_text_secondary'))
431        .padding({
432          left: $r('app.float.wh_value_12'),
433          top: $r('app.float.distance_4'),
434          bottom: $r('app.float.distance_24'),
435        });
436
437      Text($r('app.string.accessibility_risk_content'))
438        .width(ConfigData.WH_100_100)
439        .fontSize($r('app.float.font_18'))
440        .fontColor($r('sys.color.ohos_id_color_text_secondary'))
441        .padding({
442          left: $r('app.float.wh_value_12'),
443          top: $r('app.float.distance_4'),
444          bottom: $r('app.float.distance_24'),
445        });
446
447      // checkbox
448      Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
449        Checkbox()
450          .select(false)
451          .selectedColor('#4C89F0')
452          .onChange((value: boolean) => {
453            this.updateUserApproval(value);
454          });
455        Text($r('app.string.accessibility_user_approval'))
456          .fontSize($r('app.float.font_18'))
457          .baselineOffset(-20)
458          .margin({ bottom: $r('app.float.item_common_vertical_margin') });
459      }
460
461      Button($r('app.string.accessibility_disable'))
462        .backgroundColor($r('app.color.font_color_007DFF'))
463        .fontSize($r("app.float.font_20"))
464        .fontColor(Color.White)
465        .fontWeight(FontWeight.Medium)
466        .width(ComponentConfig.WH_90_100)
467        .onClick(() => this.cancelEvent());
468      Divider()
469        .color($r('sys.color.ohos_id_color_list_separator'))
470        .vertical(true)
471        .height($r('app.float.wh_value_10'))
472        .opacity($r('app.float.opacity_0_2'));
473      Button(this.title)
474        .backgroundColor(Color.White)
475        .fontSize($r("app.float.font_20"))
476        .fontColor(Color.Red)
477        .fontWeight(FontWeight.Medium)
478        .width(ComponentConfig.WH_90_100)
479        .enabled(this.enableState)
480        .opacity(this.enableState ? 1 : 0.5)
481        .onClick(() => this.confirmEvent());
482    }
483    .alignItems(HorizontalAlign.Center)
484    .width(ConfigData.WH_100_100);
485  }
486
487  updateUserApproval(value: boolean): void {
488    this.userApproval = value;
489    this.getEnableButtonState();
490  }
491
492  getUserApproval(): boolean {
493    return this.userApproval;
494  }
495
496  getEnableButtonState(): void {
497    if (this.freezingTimeForView <= 0 && this.userApproval) {
498      this.enableState = true;
499    } else {
500      this.enableState = false;
501    }
502  }
503
504  updateButtonTitle(): void {
505    this.getEnableButtonState();
506    if (this.freezingTimeForView > 0) {
507      this.title = ResourceUtils.getPluralStringValueSync($r('app.plural.user_approval_remain_times'), this.freezingTimeForView);
508    } else {
509      this.title = ResourceUtils.getCapitalStringSync($r('app.string.accessibility_enable'));
510    }
511  }
512
513  cancelEvent(): void {
514    LogUtil.info(`${MODULE_TAG} Cancel enable accessibility service`);
515    if (this.secondClickAction) {
516      this.secondClickAction();
517    }
518    this.freezingTimeForView = -1;
519    if (this.dialogController) {
520      this.dialogController.close();
521    }
522  }
523
524  confirmEvent(): void {
525    LogUtil.info(`${MODULE_TAG} Cancel enable accessibility service`);
526    if (this.firstClickAction) {
527      this.firstClickAction();
528    }
529    if (this.dialogController) {
530      this.dialogController.close();
531    }
532  }
533
534  async freezeView() {
535    if (this.countDownTime <= 0) {
536      return;
537    }
538    this.freezingTimeForView = this.countDownTime / 1000;
539    while (this.freezingTimeForView > 0) {
540      await Sleep(1000);
541      LogUtil.info(`${MODULE_TAG} time left: ${JSON.stringify(this.freezingTimeForView)}`);
542      this.freezingTimeForView -= 1;
543      this.updateButtonTitle();
544    }
545  }
546
547  aboutToAppear(): void {
548    this.freezeView();
549  }
550
551  aboutToDisappear(): void {
552    this.freezingTimeForView = -1;
553  }
554}
555
556export const Sleep = (ms: number) => {
557  return new Promise<PromiseConstructor>(resolve => setTimeout(resolve, ms));
558}