• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2025 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 { display, promptAction, window } from '@kit.ArkUI';
17import { customScan, detectBarcode, scanBarcode, scanCore } from '@kit.ScanKit';
18import { BusinessError } from '@kit.BasicServicesKit';
19import { photoAccessHelper } from '@kit.MediaLibraryKit';
20
21import { logger } from '../common/util/Logger';
22import CommonConstants from '../common/constants/CommonConstants';
23import PermissionModel from '../model/PermissionModel';
24import WindowModel from '../model/WindowModel';
25import { ScanSize } from '../model/ScanSize';
26
27@Observed
28export default class CustomScanViewModel {
29  /**
30   * 单例模型私有化构造函数,使用getInstance静态方法获得单例
31   */
32  private constructor() {
33    // 初始化窗口管理model
34    const windowStage: window.WindowStage | undefined = AppStorage.get('windowStage');
35    if (windowStage) {
36      this.windowModel.setWindowStage(windowStage);
37    }
38
39    // 初始化相机流尺寸
40    this.updateCameraCompSize();
41  }
42
43  /**
44   * CustomScanViewModel 单例
45   */
46  private static instance?: CustomScanViewModel;
47
48  /**
49   * 获取CustomScanViewModel单例实例
50   * @returns {CustomScanViewModel} CustomScanViewModel
51   */
52  static getInstance(): CustomScanViewModel {
53    if (!CustomScanViewModel.instance) {
54      CustomScanViewModel.instance = new CustomScanViewModel();
55    }
56
57    return CustomScanViewModel.instance;
58  }
59
60  /**
61   * 是否开启扫描动画
62   */
63  public isScanLine: Boolean = false;
64  /**
65   * 是否扫描成功
66   */
67  private isScannedRaw: boolean = false;
68  get isScanned () {
69    return this.isScannedRaw;
70  }
71  set isScanned (val: boolean) {
72    this.isScannedRaw = val;
73  }
74  /**
75   * 扫描结果
76   */
77  public scanResult: ScanResults = new ScanResults();
78  /**
79   * 扫描结果内容弹窗id
80   */
81  public scanResultDialogId?: number;
82  /**
83   * PermissionModel 单例
84   */
85  private permissionModel: PermissionModel = PermissionModel.getInstance();
86  /**
87   * WindowModel 单例
88   */
89  private windowModel: WindowModel = WindowModel.getInstance();
90
91  private scanSize: ScanSize = ScanSize.getInstance();
92  /**
93   * 自定义扫码初始化配置参数
94   */
95  private customScanInitOptions: scanBarcode.ScanOptions = {
96    scanTypes: [scanCore.ScanType.QR_CODE],
97    enableMultiMode: true,
98    enableAlbum: true
99  }
100  /**
101   * 当前屏幕折叠态(仅折叠屏设备下有效)
102   */
103  public curFoldStatus: display.FoldStatus = display.FoldStatus.FOLD_STATUS_UNKNOWN;
104  /**
105   * 相机流展示组件(XComponent)的surfaceId
106   */
107  public surfaceId: string = '';
108  /**
109   * 相机流展示组件(XComponent)的宽
110   */
111  public cameraCompWidth: number = 0;
112  /**
113   * 相机流展示组件(XComponent)的高
114   */
115  public cameraCompHeight: number = 0;
116  /**
117   * 相机流展示组件(XComponent)的水平偏移位置
118   */
119  public cameraCompOffsetX: number = 0;
120  /**
121   * 相机流展示组件(XComponent)的竖直偏移位置
122   */
123  public cameraCompOffsetY: number = 0;
124  /**
125   * 相机流展示组件内容尺寸修改回调函数
126   */
127  public cameraCompSizeUpdateCb: Function = (): void => {
128  };
129  /**
130   * 相机闪光灯状态更新回调函数
131   */
132  public cameraLightUpdateCb: Function = (): void => {
133  };
134
135  /**
136   * 检测是否有相机权限,未授权尝试申请授权
137   * @returns {Promise<boolean>} 相机权限/授权结果
138   */
139  async reqCameraPermission(): Promise<boolean> {
140    const reqPermissionName = CommonConstants.PERMISSION_CAMERA;
141    // 优先检测是否已授权
142    let isGranted = await this.permissionModel.checkPermission(reqPermissionName);
143    if (isGranted) {
144      return true;
145    }
146    // 没有授权申请授权
147    isGranted = await this.permissionModel.requestPermission(reqPermissionName);
148    return isGranted;
149  }
150
151  /**
152   * 当前主窗口是否开启沉浸模式
153   * @param {boolean} enable 是否开启
154   * @returns {void}
155   */
156  setMainWindowImmersive(enable: boolean): void {
157    this.windowModel.setMainWindowImmersive(enable);
158  }
159
160  /**
161   * 更新相机流展示组件(XComponent)的尺寸
162   * @returns {void}
163   */
164  async updateCameraCompSize(): Promise<void> {
165    // 通过窗口属性修改组件宽高
166    let windowSize: window.Size | null = this.scanSize.setWindowSize();
167    if (windowSize) {
168      this.scanSize.setScanXComponentSize(true, windowSize)
169
170      this.cameraCompWidth = this.scanSize.xComponentSize.width;
171      this.cameraCompHeight = this.scanSize.xComponentSize.height
172      this.cameraCompOffsetX = this.scanSize.xComponentSize.offsetX;
173      this.cameraCompOffsetY = this.scanSize.xComponentSize.offsetY;
174    }
175
176    logger.debug(`updateCameraCompSize: width=${this.cameraCompWidth} height=${this.cameraCompHeight}`);
177  }
178
179  /**
180   * 注册屏幕状态监听
181   * @returns {void}
182   */
183  regDisplayListener(): void {
184    if (display.isFoldable()) {
185      // 监听折叠屏状态变更,更新折叠态,修改窗口显示方向
186      display.on('foldStatusChange', async (curFoldStatus: display.FoldStatus) => {
187        // 无视FOLD_STATUS_UNKNOWN状态
188        if (curFoldStatus === display.FoldStatus.FOLD_STATUS_UNKNOWN) {
189          return;
190        }
191        // FOLD_STATUS_HALF_FOLDED状态当作FOLD_STATUS_EXPANDED一致处理
192        if (curFoldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED) {
193          curFoldStatus = display.FoldStatus.FOLD_STATUS_EXPANDED;
194        }
195        // 同一个状态重复触发不做处理
196        if (this.curFoldStatus === curFoldStatus) {
197          return;
198        }
199
200        // 缓存当前折叠状态
201        this.curFoldStatus = curFoldStatus;
202
203        // 当前没有相机流资源,只更新相机流宽高设置
204        if (!this.surfaceId) {
205          this.updateCameraCompSize();
206          return;
207        }
208
209        // 关闭闪光灯
210        this.tryCloseFlashLight();
211        setTimeout(() => {
212          // 重新启动扫码
213          this.restartCustomScan();
214        }, 10)
215      })
216    }
217  }
218
219  /**
220   * 注册相机流展示组件内容尺寸修改回调函数
221   * @param {Function} callback 相机流展示组件内容尺寸修改回调函数
222   * @returns {void}
223   */
224  regXCompSizeUpdateListener(callback: Function): void {
225    this.cameraCompSizeUpdateCb = callback;
226  }
227
228  /**
229   * 注册相机闪光灯状态更新回调函数
230   * @param {Function} callback 相机闪光灯状态更新回调函数
231   * @returns {void}
232   */
233  regCameraLightUpdateListener(callback: Function): void {
234    this.cameraLightUpdateCb = callback;
235  }
236
237  /**
238   * 更新相机闪光灯状态
239   * @returns {void}
240   */
241  updateFlashLightStatus(): void {
242    // 根据当前闪光灯状态,选择打开或关闭闪关灯
243    try {
244      let isCameraLightOpen: boolean = false;
245      if (customScan.getFlashLightStatus()) {
246        customScan.closeFlashLight();
247        isCameraLightOpen = false;
248      } else {
249        customScan.openFlashLight();
250        isCameraLightOpen = true;
251      }
252
253      this.cameraLightUpdateCb(isCameraLightOpen);
254    } catch (error) {
255      logger.error('flashLight control failed, error: ' + JSON.stringify(error));
256    }
257  }
258
259  /**
260   * 尝试把开启的闪光灯关闭
261   * @returns {void}
262   */
263  tryCloseFlashLight() {
264    try {
265      // 闪光灯标记移除
266      this.cameraLightUpdateCb(false);
267      // 如果闪光灯开启,则关闭
268      if (customScan.getFlashLightStatus()) {
269        customScan.closeFlashLight();
270      }
271    } catch (error) {
272      logger.error('flashLight try close failed, error: ' + JSON.stringify(error));
273    }
274  }
275
276  /**
277   * 自定义扫码数据初始化
278   * @returns {void}
279   */
280  initScanData() {
281    this.isScanLine = false;
282    this.isScanned = false;
283    this.scanResult = {} as ScanResults;
284  }
285
286  /**
287   * 初始化自定义扫码
288   * @returns {void}
289   */
290  initCustomScan(): void {
291    logger.info('initCustomScan');
292    try {
293      this.initScanData()
294      customScan.init(this.customScanInitOptions);
295      this.startCustomScan();
296    } catch (error) {
297      logger.error('init fail, error: ' + JSON.stringify(error));
298    }
299  }
300
301  /**
302   * 启动自定义扫码
303   * @returns {void}
304   */
305  startCustomScan(): void {
306    logger.info('startCustomScan');
307    try {
308      const viewControl: customScan.ViewControl = {
309        width: this.cameraCompWidth,
310        height: this.cameraCompHeight,
311        surfaceId: this.surfaceId
312      };
313      customScan.start(viewControl).then((result) => {
314        // TODO:知识点 请求扫码结果,通过Promise触发回调
315        this.customScanCallback(result);
316      }).catch((error: BusinessError) => {
317        logger.error('customScan start failed error: ' + JSON.stringify(error));
318      })
319      this.isScanLine = true;
320    } catch (error) {
321      logger.error('startCustomScan failed error: ' + JSON.stringify(error));
322    }
323  }
324
325  /**
326   * 重新触发一次扫码(仅能使用在customScan.start的异步回调中)
327   * @returns {void}
328   */
329  reCustomScan(): void {
330    try {
331      customScan.rescan();
332    } catch (error) {
333      logger.error('reCustomScan failed error: ' + JSON.stringify(error));
334    }
335  }
336
337  /**
338   * 停止自定义扫码
339   * @returns {void}
340   */
341  stopCustomScan(): void {
342    // 关闭相机闪光灯
343    this.tryCloseFlashLight();
344
345    // 关闭自定义扫码
346    try {
347      this.isScanLine = false;
348      customScan.stop().then(() => {
349      }).catch((error: BusinessError) => {
350        logger.error('customScan stop failed error: ' + JSON.stringify(error));
351      })
352    } catch (error) {
353      logger.error('stopCustomScan: stop error: ' + JSON.stringify(error));
354    }
355  }
356
357  /**
358   * 释放自定义扫码资源
359   * @returns {Promise<void>}
360   */
361  async releaseCustomScan(): Promise<void> {
362    logger.info('releaseCustomScan');
363    try {
364      await customScan.release();
365    } catch (error) {
366      logger.error('Catch: release error: ' + JSON.stringify(error));
367    }
368  }
369
370  /**
371   * 重新启动扫码
372   * @returns {void}
373   */
374  async restartCustomScan(): Promise<void> {
375    logger.info('restartCustomScan');
376    // 关闭存在的扫描结果对话框
377    this.closeScanResult();
378    // 根据窗口尺寸调整展示组件尺寸
379    await this.updateCameraCompSize();
380    // 调整相机surface尺寸
381    this.cameraCompSizeUpdateCb(this.cameraCompWidth,
382                                this.cameraCompHeight,
383                                this.cameraCompOffsetX,
384                                this.cameraCompOffsetY);
385    // 释放扫码资源
386    await this.releaseCustomScan();
387    // 初始化相机资源并启动
388    this.initCustomScan();
389  }
390
391  /**
392   * 扫码结果回调
393   * @param {scanBarcode.ScanResult[]} result 扫码结果数据
394   * @returns {void}
395   */
396  customScanCallback(result: scanBarcode.ScanResult[]): void {
397    if (!this.isScanned) {
398      this.scanResult.code = 0;
399      this.scanResult.data = result || [];
400      let resultLength: number = result ? result.length : 0;
401      if (resultLength) {
402        // 停止扫描
403        this.stopCustomScan();
404        // 标记扫描状态,触发UI刷新
405        this.isScanned = true;
406        this.scanResult.size = resultLength;
407      } else {
408        // 重新扫码
409        this.reCustomScan()
410      }
411    }
412  }
413
414  /**
415   * 关闭扫码结果
416   * @returns {void}
417   */
418  closeScanResult(): void {
419    logger.info('closeScanResult');
420    if (this.scanResultDialogId) {
421      promptAction.closeCustomDialog(this.scanResultDialogId);
422      this.scanResultDialogId = undefined;
423    }
424  }
425
426  /**
427   * 打开系统相册,选择照片进行二维码识别
428   * @returns {string}
429   */
430  async detectFromPhotoPicker(): Promise<string> {
431    const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
432    photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
433    photoSelectOptions.maxSelectNumber = 1;
434    const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
435    const photoSelectResult: photoAccessHelper.PhotoSelectResult = await photoViewPicker.select(photoSelectOptions);
436    const uris: string[] = photoSelectResult.photoUris;
437    if (uris.length === 0) {
438      return '';
439    }
440
441    // 识别结果
442    let retVal = CommonConstants.DETECT_NO_RESULT;
443    const inputImage: detectBarcode.InputImage = { uri: uris[0] };
444    try {
445      // 定义识码参数options
446      let options: scanBarcode.ScanOptions = {
447        scanTypes: [scanCore.ScanType.QR_CODE],
448        enableMultiMode: true,
449        enableAlbum: true,
450      }
451      // 调用图片识码接口
452      const decodeResult: scanBarcode.ScanResult[] = await detectBarcode.decode(inputImage, options);
453      if (decodeResult.length > 0) {
454        retVal = decodeResult[0].originalValue;
455      }
456      logger.error('[customscan]', `Failed to get ScanResult by promise with options.`);
457    } catch (error) {
458      logger.error('[customscan]', `Failed to detectBarcode. Code: ${error.code}, message: ${error.message}`);
459    }
460
461    // 停止扫描
462    this.stopCustomScan();
463    return retVal;
464  }
465}
466
467@Observed
468export class ScanResults {
469  public code: number;
470  public data: scanBarcode.ScanResult[];
471  public size: number;
472  public uri: string;
473
474  constructor() {
475    this.code = 0;
476    this.data = [];
477    this.size = 0;
478    this.uri = '';
479  }
480}
481
482
483