活体检测完成返回指定Navigation路由页面功能实现
============================

#### 问题现象

[startLivenessDetection](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/vision-interactive-liveness#section68501963545)接口的参数[InteractiveLivenessConfig](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/vision-interactive-liveness#section16532153115517)中可以设置successfulRouteUrl和failedRouteUrl分别作为人脸活体检测成功/失败后跳转的页面路径。页面路由只支持Router方式，不支持Navigation方式。

如何实现检测结束后自动跳转到Navigation路由的指定页面？

#### 背景知识

*   人脸活体检测具有两个同名startLivenessDetection接口：
    
    [startLivenessDetection](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/vision-interactive-liveness#section68501963545)跳转到人脸活体检测页面的入口，使用Promise异步回调；[startLivenessDetection](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/vision-interactive-liveness#section887319119114)跳转到人脸活体检测页面的入口，使用Promise异步回调获取跳转结果，使用callback回调获取检测结果。
    
*   [Navigation路由操作](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-navigation-navigation#路由操作)支持：页面跳转、页面替换、参数获取等多种。

#### 解决方案

实现思路：在[startLivenessDetection](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/vision-interactive-liveness#section887319119114)接口的callback回调函数中获取检测结果，再根据检测结果调用NavPathStack.pushPath()（或NavPathStack.replacePath()）方法跳转到成功/失败页面。

具体步骤：

*   定义检测结果对象，可根据业务需求自定义实现，简单示例：
    
    /\*\*
     \* 人脸识别结果对象
     \*/
    export class DetectionResult {
      // 自定义code和message
      finalResult: Record<string, number | string\>|undefined \= undefined;
    }
    

*   [startLivenessDetection](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/vision-interactive-liveness#section887319119114)接口的callback回调函数中获取检测结果：
    
    interactiveLiveness.startLivenessDetection(routerOptions, (err: BusinessError,
      result: interactiveLiveness.InteractiveLivenessResult | undefined) \=\> { // 当路由跳转错误时，获取结果失败，result返回undefined
      let detectionResult: DetectionResult \= new DetectionResult();
      if(err.code !== 0 && !result) { // 在发生错误如路由跳转失败/参数错误/权限被拒绝时，会抛出错误码，详见ArkTS API错误码
        console.error(\`LivenessCollectionIndex failed to detect. Code: ${err.code}，message: ${err.message}\`);
        detectionResult.finalResult \= {
          'code': err.code,
          'message': 'Error'
        };
    
        this.pathStack.pushPath({name: 'StartLivenessDetectionFailed', param: detectionResult});
        return;
      }
    
      console.info(\`LivenessCollectionIndex Succeeded in detecting result: ${result}\`);
    
      if (result?.mPixelMap) {
        detectionResult.finalResult \= {
          'code': 0,
          'message': 'Success'
        };
    
        this.pathStack.pushPath({name: 'StartLivenessDetectionSuccess', param: detectionResult});
      } else {
        detectionResult.finalResult \= {
          'code': -1,
          'message': 'Failed'
        };
        this.pathStack.pushPath({name: 'Detection', param: detectionResult});
      }
    });
    
*   检测结果目标页面接收路由参数。
    
    .onReady((context: NavDestinationContext) \=\> {
      this.pathStack \= context.pathStack;
      let detectionResult: DetectionResult \= this.pathStack.getParamByIndex(1) as DetectionResult;
      if (detectionResult?.finalResult) {
        let message \= detectionResult?.finalResult\['message'\];
        console.info(\`Detection result is ${message}.\`);
      } else {
        console.info(\`Detection result is succeed.\`);
      }
    });
    

完整示例如下：

*   权限声明与路由配置（src/main/module.json5）：
    
    {
      "module": {
        "name": "entry",
        "type": "entry",
        "description": "$string:module\_desc",
        "mainElement": "EntryAbility",
        "deviceTypes": \[
          "phone"
        \],
        "deliveryWithInstall": true,
        "installationFree": false,
        "pages": "$profile:main\_pages",
        "routerMap": "$profile:route\_map", // 路由配置
        "abilities": \[
          {
            "name": "EntryAbility",
            "srcEntry": "./ets/entryability/EntryAbility.ets",
            "description": "$string:EntryAbility\_desc",
            "icon": "$media:layered\_image",
            "label": "$string:EntryAbility\_label",
            "startWindowIcon": "$media:startIcon",
            "startWindowBackground": "$color:start\_window\_background",
            "exported": true,
            "skills": \[
              {
                "entities": \[
                  "entity.system.home"
                \],
                "actions": \[
                  "ohos.want.action.home"
                \]
              }
            \]
          }
        \],
        "extensionAbilities": \[
          {
            "name": "EntryBackupAbility",
            "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
            "type": "backup",
            "exported": false,
            "metadata": \[
              {
                "name": "ohos.extension.backup",
                "resource": "$profile:backup\_config"
              }
            \],
          }
        \],
        "requestPermissions": \[ // 权限声明
          {
            "name": "ohos.permission.CAMERA",
            "reason": "$string:reason\_of\_camera",
            "usedScene": {
              "when": "always"
            }
          }
        \]
      }
    }
    
*   权限原因配置（src/main/resources/base/element/string.json）：
    
    {
      "string": \[
        {
          "name": "module\_desc",
          "value": "module description"
        },
        {
          "name": "EntryAbility\_desc",
          "value": "description"
        },
        {
          "name": "EntryAbility\_label",
          "value": "label"
        },
        {
          "name": "reason\_of\_camera",
          "value": "活体检测"
        }
      \]
    }
    
*   路由配置文件（src/main/resources/base/profile/route\_map.json）：
    
    {
      "routerMap": \[
        {
          "name": "Detection",
          "pageSourceFile": "src/main/ets/pages/Detection.ets",
          "buildFunction": "DetectionBuilder",
          "data": {
            "description" : "this is Detection"
          }
        },
        {
          "name": "StartLivenessDetectionSuccess",
          "pageSourceFile": "src/main/ets/pages/StartLivenessDetectionSuccess.ets",
          "buildFunction": "StartLivenessDetectionSuccessBuilder",
          "data": {
            "description" : "this is StartLivenessDetectionSuccessBuilder"
          }
        },
        {
          "name": "StartLivenessDetectionFailed",
          "pageSourceFile": "src/main/ets/pages/StartLivenessDetectionFailed.ets",
          "buildFunction": "StartLivenessDetectionFailedBuilder",
          "data": {
            "description" : "this is StartLivenessDetectionFailedBuilder"
          }
        }
      \]
    }
    
*   首页（src/main/ets/pages/Index.ets）:
    
    @Entry
    @Component
    struct Index {
      pageInfos: NavPathStack \= new NavPathStack();
      aboutToAppear(): void {
        let param \= this.getUIContext().getRouter().getParams();
        if (param) {
          this.pageInfos.pushPath({name: 'StartLivenessDetectionSuccess'});
        }
      }
      build() {
        Navigation(this.pageInfos) {
          Column() {
            Button('去检测', { stateEffect: true, type: ButtonType.Capsule })
              .width('80%')
              .height(40)
              .margin(20)
              .onClick(() \=\> {
                this.pageInfos.pushPath({ name: 'Detection' }); //将name指定的NavDestination页面信息入栈
              })
          }
        }
        .mode(NavigationMode.Auto)
        .title('首页')
      }
    }
    
*   检测页面（src/main/ets/pages/Detection.ets）：
    
    import { common, abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
    import { interactiveLiveness } from '@kit.VisionKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    
    /\*\*
     \* 人脸识别结果对象
     \*/
    export class DetectionResult {
      // 自定义code和message
      finalResult: Record<string, number | string\>|undefined \= undefined;
    }
    
    @Builder
    export function DetectionBuilder() {
      Detection()
    }
    
    @Component
    export struct Detection {
      private context: common.UIAbilityContext \= this.getUIContext().getHostContext() as common.UIAbilityContext;
      @State pathStack: NavPathStack \= new NavPathStack();
      @State failResult: Record<string, number | string\> = {
        'code': 1008302000,
        'message': ''
      };
    
      private array: Array<Permissions\> = \['ohos.permission.CAMERA'\];
      private actionsNum: number \= 3;
      private isSilentMode: interactiveLiveness.DetectionMode.INTERACTIVE\_MODE \=
        interactiveLiveness.DetectionMode.INTERACTIVE\_MODE;
    
      build() {
        NavDestination() {
          Stack({
            alignContent: Alignment.Top
          }) {
            Column() {
              Row() {
                Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
                  Text('动作数量：' \+ this.actionsNum)
                    .fontSize(30)
                    .width('50%')
                }
              }
            }
            .margin({ left: 24, top: 80 })
            .zIndex(1)
    
            Stack({
              alignContent: Alignment.Bottom
            }) {
              Button('开始检测', { type: ButtonType.Normal, stateEffect: true })
                .width(192)
                .height(40)
                .fontSize(16)
                .backgroundColor(0x317aff)
                .borderRadius(20)
                .margin({
                  bottom: 56
                })
                .onClick(() \=\> {
                  this.startDetection();
                })
            }
            .height('100%')
          }
        }.title('检测页面')
        .onReady((context: NavDestinationContext) \=\> {
          this.pathStack \= context.pathStack;
          if (context.pathInfo.param) {
            let detectionResult \= context.pathInfo.param as DetectionResult;
            if (detectionResult.finalResult) {
              this.failResult \= detectionResult.finalResult;
            }
          }
        });
      }
    
      // 跳转到人脸活体检测控件
      private routerLibrary() {
        let routerOptions: interactiveLiveness.InteractiveLivenessConfig \= {
          isSilentMode: this.isSilentMode,
          routeMode: interactiveLiveness.RouteRedirectionMode.BACK\_MODE,
          actionsNum: this.actionsNum
        };
    
        if (canIUse('SystemCapability.AI.Component.LivenessDetect')) {
          interactiveLiveness.startLivenessDetection(routerOptions, (err: BusinessError,
            result: interactiveLiveness.InteractiveLivenessResult | undefined) \=\> { // 当路由跳转错误时，获取结果失败，result返回undefined
            let detectionResult: DetectionResult \= new DetectionResult();
            if(err.code !== 0 && !result) { // 在发生错误如路由跳转失败/参数错误/权限被拒绝时，会抛出错误码，详见ArkTS API错误码
              console.error(\`LivenessCollectionIndex failed to detect. Code: ${err.code}，message: ${err.message}\`);
              detectionResult.finalResult \= {
                'code': err.code,
                'message': 'Error'
              };
    
              this.pathStack.pushPath({name: 'StartLivenessDetectionFailed', param: detectionResult});
              return;
            }
    
            console.info(\`LivenessCollectionIndex Succeeded in detecting result: ${result}\`);
    
            if (result?.mPixelMap) {
              detectionResult.finalResult \= {
                'code': 0,
                'message': 'Success'
              };
    
              this.pathStack.pushPath({name: 'StartLivenessDetectionSuccess', param: detectionResult});
            } else {
              detectionResult.finalResult \= {
                'code': -1,
                'message': 'Failed'
              };
              this.pathStack.pushPath({name: 'Detection', param: detectionResult});
            }
          });
        } else {
          console.error('LivenessCollectionIndex this api is not supported on this device');
        }
      }
    
      // 校验CAMERA权限
      private startDetection() {
        abilityAccessCtrl.createAtManager().requestPermissionsFromUser(this.context, this.array).then((res) \=\> {
          for (let i \= 0; i < res.permissions.length; i++) {
            if (res.permissions\[i\] \=== 'ohos.permission.CAMERA' && res.authResults\[i\] \=== 0) {
              this.routerLibrary();
            }
          }
        }).catch((err: BusinessError) \=\> {
          console.error(\`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}\`);
        });
      }
    }
    
*   检测成功页面（src/main/ets/pages/StartLivenessDetectionSuccess.ets）：
    
    import { DetectionResult } from './Detection';
    
    @Builder
    export function StartLivenessDetectionSuccessBuilder() {
      StartLivenessDetectionSuccess()
    }
    
    @Component
    export struct StartLivenessDetectionSuccess {
      @State pathStack: NavPathStack \= new NavPathStack();
    
      build() {
        NavDestination() {
          Stack({
            alignContent: Alignment.Top
          }) {
            Column() {
              Stack({
                alignContent: Alignment.Bottom
              }) {
                Text('Success')
                  .fontSize(100)
                  .fontColor(Color.Green)
                  .align(Alignment.Center)
                  .margin({ bottom: 260 })
              }
            }
          }
        }
        .onReady((context: NavDestinationContext) \=\> {
          this.pathStack \= context.pathStack;
          let detectionResult: DetectionResult \= this.pathStack.getParamByIndex(1) as DetectionResult;
          if (detectionResult?.finalResult) {
            let message \= detectionResult?.finalResult\['message'\];
            console.info(\`Detection result is ${message}.\`);
          } else {
            console.info(\`Detection result is succeed.\`);
          }
        });
      }
    }
    
*   检测失败页面（src/main/ets/pages/StartLivenessDetectionFailed.ets）：
    
    import { DetectionResult } from './Detection';
    
    @Builder
    export function StartLivenessDetectionFailedBuilder() {
      StartLivenessDetectionFailed()
    }
    
    @Component
    export struct StartLivenessDetectionFailed {
      @State pathStack: NavPathStack \= new NavPathStack();
    
      build() {
        NavDestination() {
          Stack({
            alignContent: Alignment.Top
          }) {
            Column() {
              Stack({
                alignContent: Alignment.Bottom
              }) {
                Text('Failed')
                  .fontSize(100)
                  .fontColor(Color.Red)
                  .fontWeight(FontWeight.Bolder)
                  .align(Alignment.Center)
                  .margin({ bottom: 260 })
              }
            }
          }
        }
        .onReady((context: NavDestinationContext) \=\> {
          this.pathStack \= context.pathStack;
          let detectionResult: DetectionResult \= this.pathStack.getParamByIndex(1) as DetectionResult;
          if (detectionResult?.finalResult) {
            let message \= detectionResult?.finalResult\['message'\];
            console.info(\`Detection result is ${message}.\`);
          } else {
            console.info(\`Detection result is failed.\`);
          }
        });
      }
    }
    

#### 常见FAQ

Q：调用startLivenessDetection接口，没有像期望结果那样跳转到指定页面，而是跳转到默认页面，原因是什么？

A：使用callback回调获取检测结果的startLivenessDetection接口，当前只适用于RouteRedirectionMode.BACK\_MODE跳转模式。检查入参InteractiveLivenessConfig对象的routeMode属性值是否为RouteRedirectionMode.BACK\_MODE。

#### 总结

通过自行判断检测结果跳转指定页面，可以突破路由方式的限制，更灵活的实现业务需求。