9afce6f6创建于 2025年5月7日历史提交
/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { display, promptAction, window } from '@kit.ArkUI';
import { customScan, detectBarcode, scanBarcode, scanCore } from '@kit.ScanKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';

import { logger } from 'utils';

import CommonConstants from '../common/constants/CommonConstants';
import PermissionModel from '../model/PermissionModel';
import WindowModel from '../model/WindowModel';
import { ScanSize } from '../model/ScanSize';

@Observed
export default class CustomScanViewModel {
  /**
   * 单例模型私有化构造函数,使用getInstance静态方法获得单例
   */
  private constructor() {
    // 初始化窗口管理model
    const windowStage: window.WindowStage | undefined = AppStorage.get('windowStage');
    if (windowStage) {
      this.windowModel.setWindowStage(windowStage);
    }

    // 初始化相机流尺寸
    this.updateCameraCompSize();
  }

  /**
   * CustomScanViewModel 单例
   */
  private static instance?: CustomScanViewModel;

  /**
   * 获取CustomScanViewModel单例实例
   * @returns {CustomScanViewModel} CustomScanViewModel
   */
  static getInstance(): CustomScanViewModel {
    if (!CustomScanViewModel.instance) {
      CustomScanViewModel.instance = new CustomScanViewModel();
    }

    return CustomScanViewModel.instance;
  }

  /**
   * 是否开启扫描动画
   */
  public isScanLine: Boolean = false;
  /**
   * 是否扫描成功
   */
  // public isScanned: boolean = false;
  private isScannedRaw: boolean = false;
  get isScanned () {
    return this.isScannedRaw;
  }
  set isScanned (val: boolean) {
    this.isScannedRaw = val;
  }
  /**
   * 扫描结果
   */
  public scanResult: ScanResults = new ScanResults();
  /**
   * 扫描结果内容弹窗id
   */
  public scanResultDialogId?: number;
  /**
   * PermissionModel 单例
   */
  private permissionModel: PermissionModel = PermissionModel.getInstance();
  /**
   * WindowModel 单例
   */
  private windowModel: WindowModel = WindowModel.getInstance();

  private scanSize: ScanSize = ScanSize.getInstance();
  /**
   * 自定义扫码初始化配置参数
   */
  private customScanInitOptions: scanBarcode.ScanOptions = {
    scanTypes: [scanCore.ScanType.QR_CODE],
    enableMultiMode: true,
    enableAlbum: true
  }
  /**
   * 当前屏幕折叠态(仅折叠屏设备下有效)
   */
  curFoldStatus: display.FoldStatus = display.FoldStatus.FOLD_STATUS_UNKNOWN;
  /**
   * 相机流展示组件(XComponent)的surfaceId
   */
  surfaceId: string = '';
  /**
   * 相机流展示组件(XComponent)的宽
   */
  cameraCompWidth: number = 0;
  /**
   * 相机流展示组件(XComponent)的高
   */
  cameraCompHeight: number = 0;
  /**
   * 相机流展示组件(XComponent)的水平偏移位置
   */
  cameraCompOffsetX: number = 0;
  /**
   * 相机流展示组件(XComponent)的竖直偏移位置
   */
  cameraCompOffsetY: number = 0;
  /**
   * 相机流展示组件内容尺寸修改回调函数
   */
  cameraCompSizeUpdateCb: Function = (): void => {
  };
  /**
   * 相机闪光灯状态更新回调函数
   */
  cameraLightUpdateCb: Function = (): void => {
  };

  /**
   * 检测是否有相机权限,未授权尝试申请授权
   * @returns {Promise<boolean>} 相机权限/授权结果
   */
  async reqCameraPermission(): Promise<boolean> {
    const reqPermissionName = CommonConstants.PERMISSION_CAMERA;
    // 优先检测是否已授权
    let isGranted = await this.permissionModel.checkPermission(reqPermissionName);
    if (isGranted) {
      return true;
    }
    // 没有授权申请授权
    isGranted = await this.permissionModel.requestPermission(reqPermissionName);
    return isGranted;
  }

  /**
   * 当前主窗口是否开启沉浸模式
   * @param {boolean} enable 是否开启
   * @returns {void}
   */
  setMainWindowImmersive(enable: boolean): void {
    this.windowModel.setMainWindowImmersive(enable);
  }

  /**
   * 更新相机流展示组件(XComponent)的尺寸
   * @returns {void}
   */
  async updateCameraCompSize(): Promise<void> {
    // 通过窗口属性修改组件宽高
    let windowSize: window.Size | null = this.scanSize.setWindowSize();
    if (windowSize) {
      this.scanSize.setScanXComponentSize(true, windowSize)

      this.cameraCompWidth = this.scanSize.xComponentSize.width;
      this.cameraCompHeight = this.scanSize.xComponentSize.height
      this.cameraCompOffsetX = this.scanSize.xComponentSize.offsetX;
      this.cameraCompOffsetY = this.scanSize.xComponentSize.offsetY;
    }

    logger.debug(`updateCameraCompSize: width=${this.cameraCompWidth} height=${this.cameraCompHeight}`);
  }

  /**
   * 注册屏幕状态监听
   * @returns {void}
   */
  regDisplayListener(): void {
    if (display.isFoldable()) {
      // 监听折叠屏状态变更,更新折叠态,修改窗口显示方向
      display.on('foldStatusChange', async (curFoldStatus: display.FoldStatus) => {
        // 无视FOLD_STATUS_UNKNOWN状态
        if (curFoldStatus === display.FoldStatus.FOLD_STATUS_UNKNOWN) {
          return;
        }
        // FOLD_STATUS_HALF_FOLDED状态当作FOLD_STATUS_EXPANDED一致处理
        if (curFoldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED) {
          curFoldStatus = display.FoldStatus.FOLD_STATUS_EXPANDED;
        }
        // 同一个状态重复触发不做处理
        if (this.curFoldStatus === curFoldStatus) {
          return;
        }

        // 缓存当前折叠状态
        this.curFoldStatus = curFoldStatus;

        // 当前没有相机流资源,只更新相机流宽高设置
        if (!this.surfaceId) {
          this.updateCameraCompSize();
          return;
        }

        // 关闭闪光灯
        this.tryCloseFlashLight();
        setTimeout(() => {
          // 重新启动扫码
          this.restartCustomScan();
        }, 10)
      })
    }
  }

  /**
   * 注册相机流展示组件内容尺寸修改回调函数
   * @param {Function} callback 相机流展示组件内容尺寸修改回调函数
   * @returns {void}
   */
  regXCompSizeUpdateListener(callback: Function): void {
    this.cameraCompSizeUpdateCb = callback;
  }

  /**
   * 注册相机闪光灯状态更新回调函数
   * @param {Function} callback 相机闪光灯状态更新回调函数
   * @returns {void}
   */
  regCameraLightUpdateListener(callback: Function): void {
    this.cameraLightUpdateCb = callback;
  }

  /**
   * 更新相机闪光灯状态
   * @returns {void}
   */
  updateFlashLightStatus(): void {
    // 根据当前闪光灯状态,选择打开或关闭闪关灯
    try {
      let isCameraLightOpen: boolean = false;
      console.log("getFlashLightStatus: ", customScan.getFlashLightStatus())
      if (customScan.getFlashLightStatus()) {
        customScan.closeFlashLight();
        isCameraLightOpen = false;
      } else {
        customScan.openFlashLight();
        isCameraLightOpen = true;
      }

      this.cameraLightUpdateCb(isCameraLightOpen);
    } catch (error) {
      logger.error('flashLight control failed, error: ' + JSON.stringify(error));
    }
  }

  /**
   * 尝试把开启的闪光灯关闭
   * @returns {void}
   */
  tryCloseFlashLight() {
    try {
      // 闪光灯标记移除
      this.cameraLightUpdateCb(false);
      // 如果闪光灯开启,则关闭
      if (customScan.getFlashLightStatus()) {
        customScan.closeFlashLight();
      }
    } catch (error) {
      logger.error('flashLight try close failed, error: ' + JSON.stringify(error));
    }
  }

  /**
   * 自定义扫码数据初始化
   * @returns {void}
   */
  initScanData() {
    this.isScanLine = false;
    this.isScanned = false;
    this.scanResult = {} as ScanResults;
  }

  /**
   * 初始化自定义扫码
   * @returns {void}
   */
  initCustomScan(): void {
    logger.info('initCustomScan');
    try {
      this.initScanData()
      customScan.init(this.customScanInitOptions);
      this.startCustomScan();
    } catch (error) {
      logger.error('init fail, error: ' + JSON.stringify(error));
    }
  }

  /**
   * 启动自定义扫码
   * @returns {void}
   */
  startCustomScan(): void {
    logger.info('startCustomScan');
    try {
      const viewControl: customScan.ViewControl = {
        width: this.cameraCompWidth,
        height: this.cameraCompHeight,
        surfaceId: this.surfaceId
      };
      customScan.start(viewControl).then((result) => {
        // TODO:知识点 请求扫码结果,通过Promise触发回调
        this.customScanCallback(result);
      }).catch((error: BusinessError) => {
        logger.error('customScan start failed error: ' + JSON.stringify(error));
      })
      this.isScanLine = true;
    } catch (error) {
      logger.error('startCustomScan failed error: ' + JSON.stringify(error));
    }
  }

  /**
   * 重新触发一次扫码(仅能使用在customScan.start的异步回调中)
   * @returns {void}
   */
  reCustomScan(): void {
    try {
      customScan.rescan();
    } catch (error) {
      logger.error('reCustomScan failed error: ' + JSON.stringify(error));
    }
  }

  /**
   * 停止自定义扫码
   * @returns {void}
   */
  stopCustomScan(): void {
    // 关闭相机闪光灯
    this.tryCloseFlashLight();

    // 关闭自定义扫码
    try {
      this.isScanLine = false;
      customScan.stop().then(() => {
      }).catch((error: BusinessError) => {
        logger.error('customScan stop failed error: ' + JSON.stringify(error));
      })
    } catch (error) {
      logger.error('stopCustomScan: stop error: ' + JSON.stringify(error));
    }
  }

  /**
   * 释放自定义扫码资源
   * @returns {Promise<void>}
   */
  async releaseCustomScan(): Promise<void> {
    logger.info('releaseCustomScan');
    try {
      await customScan.release();
    } catch (error) {
      logger.error('Catch: release error: ' + JSON.stringify(error));
    }
  }

  /**
   * 重新启动扫码
   * @returns {void}
   */
  async restartCustomScan(): Promise<void> {
    logger.info('restartCustomScan');
    // 关闭存在的扫描结果对话框
    this.closeScanResult();
    // 根据窗口尺寸调整展示组件尺寸
    await this.updateCameraCompSize();
    // 调整相机surface尺寸
    this.cameraCompSizeUpdateCb(this.cameraCompWidth, this.cameraCompHeight);
    // 释放扫码资源
    await this.releaseCustomScan();
    // 初始化相机资源并启动
    this.initCustomScan();
  }

  /**
   * 扫码结果回调
   * @param {Array<scanBarcode.ScanResult>} result 扫码结果数据
   * @returns {void}
   */
  customScanCallback(result: scanBarcode.ScanResult[]): void {
    if (!this.isScanned) {
      this.scanResult.code = 0;
      this.scanResult.data = result || [];
      let resultLength: number = result ? result.length : 0;
      if (resultLength) {
        // 停止扫描
        this.stopCustomScan();
        // 标记扫描状态,触发UI刷新
        this.isScanned = true;
        this.scanResult.size = resultLength;
      } else {
        // 重新扫码
        this.reCustomScan()
      }
    }
  }

  /**
   * 关闭扫码结果
   * @returns {void}
   */
  closeScanResult(): void {
    logger.info('closeScanResult');
    if (this.scanResultDialogId) {
      promptAction.closeCustomDialog(this.scanResultDialogId);
      this.scanResultDialogId = undefined;
    }
  }

  /**
   * 打开系统相册,选择照片进行二维码识别
   * @returns {string}
   */
  async detectFromPhotoPicker(): Promise<string> {
    const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
    photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
    photoSelectOptions.maxSelectNumber = 1;
    const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
    const photoSelectResult: photoAccessHelper.PhotoSelectResult = await photoViewPicker.select(photoSelectOptions);
    const uris: Array<string> = photoSelectResult.photoUris;
    if (uris.length === 0) {
      return '';
    }

    // 识别结果
    let retVal = CommonConstants.DETECT_NO_RESULT;
    const inputImage: detectBarcode.InputImage = { uri: uris[0] };
    try {
      // 定义识码参数options
      let options: scanBarcode.ScanOptions = {
        scanTypes: [scanCore.ScanType.QR_CODE],
        enableMultiMode: true,
        enableAlbum: true,
      }
      // 调用图片识码接口
      const decodeResult: Array<scanBarcode.ScanResult> = await detectBarcode.decode(inputImage, options);
      if (decodeResult.length > 0) {
        retVal = decodeResult[0].originalValue;
      }
      logger.error('[customscan]', `Failed to get ScanResult by promise with options.`);
    } catch (error) {
      logger.error('[customscan]', `Failed to detectBarcode. Code: ${error.code}, message: ${error.message}`);
    }

    // 停止扫描
    this.stopCustomScan();
    return retVal;
  }
}

@Observed
export class ScanResults {
  public code: number;
  public data: Array<scanBarcode.ScanResult>;
  public size: number;
  public uri: string;

  constructor() {
    this.code = 0;
    this.data = [];
    this.size = 0;
    this.uri = '';
  }
}