|
@@ -1,5 +1,4 @@
|
|
|
import 'dart:async';
|
|
|
-import 'dart:convert';
|
|
|
import 'package:camera/camera.dart';
|
|
|
import 'package:fis_jsonrpc/rpc.dart';
|
|
|
import 'package:flutter/foundation.dart';
|
|
@@ -7,23 +6,19 @@ import 'package:flutter/material.dart';
|
|
|
import 'package:flutter/services.dart';
|
|
|
import 'package:get/get.dart';
|
|
|
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
|
|
-import 'package:image_gallery_saver/image_gallery_saver.dart';
|
|
|
import 'package:vitalapp/architecture/storage/storage.dart';
|
|
|
-import 'package:vitalapp/architecture/utils/datetime.dart';
|
|
|
import 'package:vitalapp/architecture/utils/prompt_box.dart';
|
|
|
import 'package:vitalapp/managers/interfaces/cache.dart';
|
|
|
import 'package:fis_common/logger/logger.dart';
|
|
|
import 'package:vitalapp/managers/interfaces/patient.dart';
|
|
|
import 'package:vitalapp/rpc.dart';
|
|
|
import 'package:vitalapp/store/store.dart';
|
|
|
-import 'dart:ui' as ui;
|
|
|
import 'index.dart';
|
|
|
|
|
|
class IdCardScanController extends GetxController with WidgetsBindingObserver {
|
|
|
IdCardScanController();
|
|
|
|
|
|
final state = IdCardScanState();
|
|
|
- final _patientManager = Get.find<IPatientManager>();
|
|
|
List<CameraDescription> _cameras = <CameraDescription>[];
|
|
|
List<CameraDescription> get cameras => _cameras;
|
|
|
|
|
@@ -38,9 +33,6 @@ class IdCardScanController extends GetxController with WidgetsBindingObserver {
|
|
|
// 屏幕上手指数量
|
|
|
int pointers = 0;
|
|
|
|
|
|
- // 当前身份证信息
|
|
|
- IdCardInfoModel idCardInfo = IdCardInfoModel();
|
|
|
-
|
|
|
/// 开始缩放
|
|
|
void handleScaleStart(ScaleStartDetails details) {
|
|
|
_baseScale = _currentScale;
|
|
@@ -252,91 +244,6 @@ class IdCardScanController extends GetxController with WidgetsBindingObserver {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// 测试图像文件缓存,print 遍历输出
|
|
|
- void debugShowCache() async {
|
|
|
- final cacheManager = Get.find<ICacheManager>();
|
|
|
- double cacheSize = await cacheManager.getCacheSize();
|
|
|
- double imageCacheSize = await cacheManager.getImageCacheSize();
|
|
|
- debugPrint('cacheSize = $cacheSize : ${formatSize(cacheSize)}');
|
|
|
- debugPrint(
|
|
|
- 'imageCacheSize = $imageCacheSize : ${formatSize(imageCacheSize)}');
|
|
|
- }
|
|
|
-
|
|
|
- /// 文件大小转为可读 Str
|
|
|
- static String formatSize(double value) {
|
|
|
- List<String> unitArr = ['B', 'K', 'M', 'G'];
|
|
|
- int index = 0;
|
|
|
- while (value > 1024) {
|
|
|
- index++;
|
|
|
- value = value / 1024;
|
|
|
- }
|
|
|
- String size = value.toStringAsFixed(2);
|
|
|
- return size + unitArr[index];
|
|
|
- }
|
|
|
-
|
|
|
- /// 保存到相册
|
|
|
- void saveImageToGallery(XFile image) async {
|
|
|
- // 获取图像的字节数据
|
|
|
- Uint8List bytes = await image.readAsBytes();
|
|
|
-
|
|
|
- // 将图像保存到相册
|
|
|
- await ImageGallerySaver.saveImage(bytes, quality: 100);
|
|
|
- }
|
|
|
-
|
|
|
- /// 处理图像裁切
|
|
|
- Future<String> clipLocalImage(XFile soureceImage, double scale) async {
|
|
|
- assert(scale >= 1, 'scale must be greater than 1');
|
|
|
- // 获取图像的字节数据
|
|
|
- Uint8List bytes = await soureceImage.readAsBytes();
|
|
|
- var codec = await ui.instantiateImageCodec(bytes);
|
|
|
- var nextFrame = await codec.getNextFrame();
|
|
|
- var image = nextFrame.image;
|
|
|
- Rect src = Rect.fromLTWH(
|
|
|
- (scale - 1) / 2 / scale * image.width.toDouble(),
|
|
|
- (scale - 1) / 2 / scale * image.height.toDouble(),
|
|
|
- image.width.toDouble() / scale,
|
|
|
- image.height.toDouble() / scale,
|
|
|
- );
|
|
|
- Rect dst =
|
|
|
- Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
|
|
|
- ui.Image croppedImage = await getCroppedImage(image, src, dst);
|
|
|
- ByteData? newImageBytes =
|
|
|
- await croppedImage.toByteData(format: ui.ImageByteFormat.png);
|
|
|
- if (newImageBytes == null) {
|
|
|
- return '';
|
|
|
- }
|
|
|
- Uint8List newImageUint8List = newImageBytes.buffer.asUint8List();
|
|
|
-
|
|
|
- /// FIXME 不要存到相册而是存到临时目录
|
|
|
- Map<Object?, Object?> result =
|
|
|
- await ImageGallerySaver.saveImage(newImageUint8List, quality: 100);
|
|
|
- String jsonString = jsonEncode(result);
|
|
|
- Map<String, dynamic> json = jsonDecode(jsonString);
|
|
|
- String? filePath = json['filePath'];
|
|
|
- return filePath ?? '';
|
|
|
- }
|
|
|
-
|
|
|
- /// 获取图像文件的图像尺寸
|
|
|
- Future<Size> getImageSize(XFile soureceImage) async {
|
|
|
- // 获取图像的字节数据
|
|
|
- Uint8List bytes = await soureceImage.readAsBytes();
|
|
|
- var codec = await ui.instantiateImageCodec(bytes);
|
|
|
- var nextFrame = await codec.getNextFrame();
|
|
|
- var image = nextFrame.image;
|
|
|
- return Size(image.width.toDouble(), image.height.toDouble());
|
|
|
- }
|
|
|
-
|
|
|
- /// 获取裁切后的图像
|
|
|
- Future<ui.Image> getCroppedImage(ui.Image image, Rect src, Rect dst) {
|
|
|
- var pictureRecorder = ui.PictureRecorder();
|
|
|
- Canvas canvas = Canvas(pictureRecorder);
|
|
|
- canvas.drawImageRect(image, src, dst, Paint());
|
|
|
- return pictureRecorder.endRecording().toImage(
|
|
|
- dst.width.floor(),
|
|
|
- dst.height.floor(),
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
/// 发生拍摄身份证事件
|
|
|
void onCaptureIdCardButtonPressed() {
|
|
|
state.isIdCardScanning = true;
|
|
@@ -344,7 +251,6 @@ class IdCardScanController extends GetxController with WidgetsBindingObserver {
|
|
|
// imageFile = file;
|
|
|
if (file != null) {
|
|
|
try {
|
|
|
- // await clipLocalImage(file, 1.8);
|
|
|
final url = await rpc.storage.upload(
|
|
|
file,
|
|
|
fileType: 'png',
|
|
@@ -357,7 +263,6 @@ class IdCardScanController extends GetxController with WidgetsBindingObserver {
|
|
|
);
|
|
|
if (result.isSuccess) {
|
|
|
PromptBox.toast('身份证识别成功');
|
|
|
- idCardInfo.localCardImagePath = file.path;
|
|
|
final idCardScanResult = IdCardScanResult(
|
|
|
success: true,
|
|
|
patientBaseDTO: result,
|
|
@@ -377,183 +282,6 @@ class IdCardScanController extends GetxController with WidgetsBindingObserver {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- /// 发生人脸录入事件
|
|
|
- // void onCaptureFaceButtonPressed() async {
|
|
|
- // state.isRunningFaceInput = true;
|
|
|
- // if (kCameraController == null) {
|
|
|
- // state.isRunningFaceInput = false;
|
|
|
- // return;
|
|
|
- // }
|
|
|
- // try {
|
|
|
- // final XFile? file = await takePicture();
|
|
|
- // if (file != null) {
|
|
|
- // faceDetector = FaceDetector(options: FaceDetectorOptions());
|
|
|
- // // faceDetector =
|
|
|
- // // FaceDetector(options: FaceDetectorOptions(enableContours: true));
|
|
|
- // int faceNum = 1; // max 分辨率下检测用时大约 100ms
|
|
|
- // // int faceNum =
|
|
|
- // // await doDetection(faceDetector, file.path); // max 分辨率下检测用时大约 100ms
|
|
|
-
|
|
|
- // if (faceNum == 0) {
|
|
|
- // PromptBox.toast('请将面部保持在识别框内');
|
|
|
- // return;
|
|
|
- // } else if (faceNum > 1) {
|
|
|
- // PromptBox.toast('请保持只有一张面部在识别范围内');
|
|
|
- // return;
|
|
|
- // }
|
|
|
-
|
|
|
- // /// TODO 上传图像到云然后传给后端
|
|
|
- // final url = await rpc.storage.upload(
|
|
|
- // file,
|
|
|
- // fileType: 'png',
|
|
|
- // );
|
|
|
- // print('⭐⭐⭐⭐⭐⭐⭐⭐ url: $url');
|
|
|
- // try {
|
|
|
- // SavePersonDTO result =
|
|
|
- // await rpc.patient.savePatientBaseByFaceImageAsync(
|
|
|
- // SavePatientBaseByFaceImageRequest(
|
|
|
- // cardNo: idCardInfo.idCardNumber,
|
|
|
- // token: Store.user.token,
|
|
|
- // image: url,
|
|
|
- // ),
|
|
|
- // );
|
|
|
- // print(result);
|
|
|
- // if (result.success) {
|
|
|
- // PromptBox.toast('人脸数据存入成功');
|
|
|
- // final idCardScanResult = IdCardScanResult(
|
|
|
- // success: true,
|
|
|
- // cardNo: idCardInfo.idCardNumber,
|
|
|
- // name: idCardInfo.idCardName,
|
|
|
- // nation: idCardInfo.idCardNation,
|
|
|
- // gender: idCardInfo.idCardGender == '男'
|
|
|
- // ? GenderEnum.Male
|
|
|
- // : GenderEnum.Female,
|
|
|
- // birthday: DateTime.parse(idCardInfo.idCardBirthDate),
|
|
|
- // address: idCardInfo.idCardAddress,
|
|
|
- // );
|
|
|
-
|
|
|
- // Get.back<IdCardScanResult>(
|
|
|
- // result: idCardScanResult,
|
|
|
- // );
|
|
|
- // } else {
|
|
|
- // PromptBox.toast('人脸数据存入失败');
|
|
|
- // }
|
|
|
- // } catch (e) {
|
|
|
- // logger.e("savePatientBaseByFaceImageAsync failed: $e", e);
|
|
|
- // }
|
|
|
- // }
|
|
|
- // } catch (e) {
|
|
|
- // logger.e("doDetection failed: $e", e);
|
|
|
- // }
|
|
|
- // state.isRunningFaceInput = false;
|
|
|
- // }
|
|
|
-
|
|
|
- /// 发生结束录制视频事件
|
|
|
- void onStopButtonPressed() {
|
|
|
- stopVideoRecording().then((XFile? file) {
|
|
|
- if (file != null) {
|
|
|
- PromptBox.toast('Video recorded to ${file.path}');
|
|
|
- // videoFile = file;
|
|
|
- // _startVideoPlayer();
|
|
|
- }
|
|
|
- update();
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- /// 发生开始录制视频事件
|
|
|
- void onVideoRecordButtonPressed() {
|
|
|
- startVideoRecording().then((_) {
|
|
|
- update();
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- /// 暂停录制视频
|
|
|
- void onPauseButtonPressed() {
|
|
|
- pauseVideoRecording().then((_) {
|
|
|
- update();
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- /// 恢复视频录制
|
|
|
- void onResumeButtonPressed() {
|
|
|
- resumeVideoRecording().then((_) {
|
|
|
- update();
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- /// 开始录制视频
|
|
|
- Future<void> startVideoRecording() async {
|
|
|
- final CameraController? cameraController = kCameraController;
|
|
|
-
|
|
|
- if (cameraController == null || !cameraController.value.isInitialized) {
|
|
|
- PromptBox.toast('Error: select a camera first.');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (cameraController.value.isRecordingVideo) {
|
|
|
- // A recording is already started, do nothing.
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- await cameraController.startVideoRecording();
|
|
|
- } on CameraException catch (e) {
|
|
|
- PromptBox.toast('Error: ${e.code}\n${e.description}');
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /// 停止录制视频
|
|
|
- Future<XFile?> stopVideoRecording() async {
|
|
|
- final CameraController? cameraController = kCameraController;
|
|
|
-
|
|
|
- if (cameraController == null || !cameraController.value.isRecordingVideo) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- return cameraController.stopVideoRecording();
|
|
|
- } on CameraException catch (e) {
|
|
|
- PromptBox.toast('Error: ${e.code}\n${e.description}');
|
|
|
-
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /// 暂停录制视频
|
|
|
- Future<void> pauseVideoRecording() async {
|
|
|
- final CameraController? cameraController = kCameraController;
|
|
|
-
|
|
|
- if (cameraController == null || !cameraController.value.isRecordingVideo) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- await cameraController.pauseVideoRecording();
|
|
|
- } on CameraException catch (e) {
|
|
|
- PromptBox.toast('Error: ${e.code}\n${e.description}');
|
|
|
-
|
|
|
- rethrow;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /// 恢复视频录制
|
|
|
- Future<void> resumeVideoRecording() async {
|
|
|
- final CameraController? cameraController = kCameraController;
|
|
|
-
|
|
|
- if (cameraController == null || !cameraController.value.isRecordingVideo) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- await cameraController.resumeVideoRecording();
|
|
|
- } on CameraException catch (e) {
|
|
|
- PromptBox.toast('Error: ${e.code}\n${e.description}');
|
|
|
-
|
|
|
- rethrow;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
/// 在 widget 内存中分配后立即调用。
|
|
|
@override
|
|
|
void onInit() async {
|
|
@@ -576,7 +304,6 @@ class IdCardScanController extends GetxController with WidgetsBindingObserver {
|
|
|
super.onClose();
|
|
|
final cacheManager = Get.find<ICacheManager>();
|
|
|
cacheManager.clearApplicationImageCache();
|
|
|
- closeDetector();
|
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
|
final CameraController? cameraController = kCameraController;
|
|
|
if (cameraController != null) {
|
|
@@ -609,177 +336,6 @@ class IdCardScanController extends GetxController with WidgetsBindingObserver {
|
|
|
this.state.isCameraReady = true;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- /// 完成人脸识别
|
|
|
- void finishFaceDetection() {
|
|
|
- // final result = IdCardScanResult(
|
|
|
- // success: true,
|
|
|
- // cardNo: idCardInfo.idCardNumber,
|
|
|
- // name: idCardInfo.idCardName,
|
|
|
- // nation: idCardInfo.idCardNation,
|
|
|
- // gender:
|
|
|
- // idCardInfo.idCardGender == '男' ? GenderEnum.Male : GenderEnum.Female,
|
|
|
- // birthday: DateTime.now(),
|
|
|
- // address: idCardInfo.idCardAddress,
|
|
|
- // );
|
|
|
-
|
|
|
- // Get.back<IdCardScanResult>(
|
|
|
- // result: result,
|
|
|
- // );
|
|
|
- }
|
|
|
-
|
|
|
- /// WIP
|
|
|
- /// 面部识别 基于 Google's ML Kit
|
|
|
- ///
|
|
|
-
|
|
|
- InputImage inputImage = InputImage.fromFilePath('');
|
|
|
- FaceDetector faceDetector = FaceDetector(options: FaceDetectorOptions());
|
|
|
-
|
|
|
- // 进行一次人脸检测
|
|
|
- Future<void> doDetection(
|
|
|
- FaceDetector faceDetector,
|
|
|
- String imagePath,
|
|
|
- ) async {
|
|
|
- inputImage = InputImage.fromFilePath(imagePath);
|
|
|
- // inputImage = image;
|
|
|
- final List<Face> faces = await faceDetector.processImage(inputImage);
|
|
|
- kFrameFacesResult = [];
|
|
|
- kFrameFacesResult.addAll(faces);
|
|
|
- // for (Face face in faces) {
|
|
|
- // final Rect boundingBox = face.boundingBox;
|
|
|
-
|
|
|
- // final double? rotX =
|
|
|
- // face.headEulerAngleX; // Head is tilted up and down rotX degrees
|
|
|
- // final double? rotY =
|
|
|
- // face.headEulerAngleY; // Head is rotated to the right rotY degrees
|
|
|
- // final double? rotZ =
|
|
|
- // face.headEulerAngleZ; // Head is tilted sideways rotZ degrees
|
|
|
-
|
|
|
- // // If landmark detection was enabled with FaceDetectorOptions (mouth, ears,
|
|
|
- // // eyes, cheeks, and nose available):
|
|
|
- // final FaceLandmark? leftEar = face.landmarks[FaceLandmarkType.leftEar];
|
|
|
- // if (leftEar != null) {
|
|
|
- // final Point<int> leftEarPos = leftEar.position;
|
|
|
- // }
|
|
|
-
|
|
|
- // // If classification was enabled with FaceDetectorOptions:
|
|
|
- // if (face.smilingProbability != null) {
|
|
|
- // final double? smileProb = face.smilingProbability;
|
|
|
- // }
|
|
|
-
|
|
|
- // // If face tracking was enabled with FaceDetectorOptions:
|
|
|
- // if (face.trackingId != null) {
|
|
|
- // final int? id = face.trackingId;
|
|
|
- // }
|
|
|
- // }
|
|
|
- }
|
|
|
-
|
|
|
- // bool isDetectionRunning = false;
|
|
|
- Timer? _detectionTimer;
|
|
|
-
|
|
|
- /// 开始持续检测人脸
|
|
|
- void runDetectionTimer() {
|
|
|
- if (_detectionTimer != null) {
|
|
|
- _detectionTimer!.cancel();
|
|
|
- _detectionTimer = null;
|
|
|
- faceDetector.close();
|
|
|
- state.isShowIdCardScanResult = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- faceDetector =
|
|
|
- FaceDetector(options: FaceDetectorOptions(enableContours: true));
|
|
|
- state.isShowIdCardScanResult = true;
|
|
|
-
|
|
|
- /// 记录最后一次拍摄的时间
|
|
|
- int lastCaptureTime = DateTime.now().millisecondsSinceEpoch;
|
|
|
- _detectionTimer = Timer.periodic(
|
|
|
- const Duration(milliseconds: 300), // max 分辨率下拍摄用时大约 500ms-800ms
|
|
|
- (timer) async {
|
|
|
- if (kCameraController == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
- final XFile? file = await takePicture();
|
|
|
- if (file != null) {
|
|
|
- if (timer.tick == 1 || kFrameImageSize == Size.zero) {
|
|
|
- Size imageSize = await getImageSize(file);
|
|
|
- kFrameImageSize = imageSize;
|
|
|
- }
|
|
|
- int kTime = DateTime.now().millisecondsSinceEpoch;
|
|
|
- print('⭐⭐⭐⭐⭐⭐⭐⭐ capture time: ${kTime - lastCaptureTime} ms');
|
|
|
- lastCaptureTime = kTime;
|
|
|
-
|
|
|
- /// 记录用时 ms
|
|
|
- await doDetection(faceDetector, file.path); // max 分辨率下检测用时大约 100ms
|
|
|
- int endTime = DateTime.now().millisecondsSinceEpoch;
|
|
|
- print('⭐⭐⭐⭐⭐⭐⭐⭐ detection time: ${endTime - lastCaptureTime} ms');
|
|
|
- update(['face_bounding_box']);
|
|
|
- if (timer.tick >= 10) {
|
|
|
- finishFaceDetection(); // TODO 接入真实的判断条件
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- /// 用于将读取的视频流传给 Google ML
|
|
|
- InputImage cameraImageToInputImage(CameraImage cameraImage) {
|
|
|
- return InputImage.fromBytes(
|
|
|
- bytes: _concatenatePlanes(cameraImage.planes),
|
|
|
- metadata: InputImageMetadata(
|
|
|
- size: Size(cameraImage.width.toDouble(), cameraImage.height.toDouble()),
|
|
|
- rotation: InputImageRotation.rotation0deg,
|
|
|
- format: _getInputImageFormat(cameraImage.format.group),
|
|
|
- bytesPerRow: cameraImage.planes[0].bytesPerRow,
|
|
|
- ),
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- /// 辅助函数,将CameraImage的plane组合为Uint8List格式
|
|
|
- Uint8List _concatenatePlanes(List<Plane> planes) {
|
|
|
- final WriteBuffer allBytes = WriteBuffer();
|
|
|
- for (Plane plane in planes) {
|
|
|
- allBytes.putUint8List(plane.bytes);
|
|
|
- }
|
|
|
- return allBytes.done().buffer.asUint8List();
|
|
|
- }
|
|
|
-
|
|
|
- InputImageFormat _getInputImageFormat(ImageFormatGroup format) {
|
|
|
- switch (format) {
|
|
|
- case ImageFormatGroup.yuv420:
|
|
|
- return InputImageFormat.yuv420;
|
|
|
- case ImageFormatGroup.bgra8888:
|
|
|
- return InputImageFormat.bgra8888;
|
|
|
- default:
|
|
|
- throw ArgumentError('Invalid image format');
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /// 销毁检测器
|
|
|
- void closeDetector() {
|
|
|
- if (_detectionTimer != null) {
|
|
|
- state.isShowIdCardScanResult = false;
|
|
|
- _detectionTimer!.cancel();
|
|
|
- _detectionTimer = null;
|
|
|
- }
|
|
|
- faceDetector.close();
|
|
|
- }
|
|
|
-
|
|
|
- Future<void> _createPatient(PatientBaseDTO result) async {
|
|
|
- var createPatientRequest = CreatePatientRequest(
|
|
|
- cardNo: idCardInfo.idCardNumber,
|
|
|
- patientName: idCardInfo.idCardName,
|
|
|
- phone: '',
|
|
|
- patientGender:
|
|
|
- idCardInfo.idCardGender == "男" ? GenderEnum.Male : GenderEnum.Female,
|
|
|
- nationality: idCardInfo.idCardNation,
|
|
|
- birthday: result.birthday!.toUtc(),
|
|
|
- cardType: CardTypeEnum.Identity,
|
|
|
- patientAddress: idCardInfo.idCardAddress,
|
|
|
- permanentResidenceAddress: idCardInfo.idCardAddress,
|
|
|
- crowdLabels: [],
|
|
|
- );
|
|
|
- await _patientManager.create(createPatientRequest);
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
class IdCardScanResult {
|