├── .gitignore ├── README.md ├── app ├── build.gradle ├── local.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── netease │ │ └── nis │ │ └── alivedetecteddemo │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── blink_eyes.wav │ │ ├── open_mouth.wav │ │ ├── pic_error_742.zip │ │ ├── turn_head_to_left.wav │ │ └── turn_head_to_right.wav │ ├── java │ │ └── com │ │ │ └── netease │ │ │ └── nis │ │ │ └── alivedetecteddemo │ │ │ ├── App.kt │ │ │ ├── FailureActivity.kt │ │ │ ├── H5WebViewActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── SuccessActivity.kt │ │ │ ├── WebViewActivity.kt │ │ │ ├── WelcomeActivity.kt │ │ │ ├── manager │ │ │ ├── BroadcastDispatcher.kt │ │ │ └── Toast.kt │ │ │ └── utils │ │ │ └── Util.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── btn_shape_success.xml │ │ ├── btn_start_alive_detected.xml │ │ ├── circle_tv_focus.xml │ │ ├── circle_tv_un_focus.xml │ │ ├── ic_launcher_background.xml │ │ ├── ll_shape_copy_token.xml │ │ ├── open_eyes.gif │ │ ├── open_mouth.gif │ │ ├── turn_left.gif │ │ └── turn_right.gif │ │ ├── layout │ │ ├── activity_failure.xml │ │ ├── activity_main.xml │ │ ├── activity_success.xml │ │ ├── activity_webview.xml │ │ ├── activity_webview_h5.xml │ │ ├── activity_welcome.xml │ │ └── layout_tv_step.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── alive_bg.png │ │ ├── app_icon.png │ │ ├── fail.png │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ ├── ico_back_2x.png │ │ ├── ico_green_2x.png │ │ ├── ico_logo_bar_blue.png │ │ ├── ico_voice_close_2x.png │ │ ├── ico_voice_open_2x.png │ │ ├── pic_demo_2x.png │ │ ├── pic_front_2x.png │ │ └── success_2x.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── face_outline.png │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── netease │ └── nis │ └── alivedetecteddemo │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── 活体检测SDK接入说明文档.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea/ 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 活体检测 2 | 根据提示做出相应动作,SDK 实时采集动态信息,判断用户是否为活体、真人 3 | 4 | ## 兼容性 5 | | 条目 | 说明 | 6 | | ----------- | ----------------------------------------------------------------------- | 7 | | 适配版本 | minSdkVersion 21 及以上版本 | 8 | | cpu 架构 | 内部提供了 armeabi-v7a 和 arm64-v8a 两种 so ,对于不兼容 arm 的 x86 机型不适配 | 9 | 10 | ## 注意事项 11 | ***和其他易盾产品一起使用需要考虑版本兼容性,若同时接多个易盾sdk,需要测试回归下是否有异常*** 12 | 13 | ## 资源引入 14 | 15 | ### 远程仓库依赖(推荐) 16 | 从 2.2.2 版本开始,提供远程依赖的方式,本地依赖的方式逐步淘汰。本地依赖集成替换为远程依赖请先去除干净本地包,避免重复依赖冲突 17 | 18 | 确认 Project 根目录的 build.gradle 中配置了 mavenCentral 支持 19 | 20 | ``` 21 | buildscript { 22 | repositories { 23 | mavenCentral() 24 | } 25 | ... 26 | } 27 | 28 | allprojects { 29 | repositories { 30 | mavenCentral() 31 | } 32 | } 33 | ``` 34 | 在对应 module 的 build.gradle 中添加依赖 35 | 36 | ``` 37 | implementation 'io.github.yidun:livedetect:3.3.2.2' 38 | ``` 39 | ### 本地手动依赖 40 | 41 | #### 获取 SDK 42 | 43 | 从易盾官网下载活体检测 sdk 的 aar 包 [包地址](https://support.dun.163.com/documents/391676076156063744?docId=391718656914821120) 44 | 45 | #### 添加 aar 包依赖 46 | 47 | 将获取到的 aar 文件拷贝到对应 module 的 libs 文件夹下(如没有该目录需新建),然后在 build.gradle 文件中增加如下代码 48 | 49 | ``` 50 | android{ 51 | repositories { 52 | flatDir { 53 | dirs 'libs' 54 | } 55 | } 56 | } 57 | 58 | dependencies { 59 | implementation(name:'livedetect', ext: 'aar') 60 | implementation(name: 'crashreport', ext: 'aar') 61 | implementation(name: 'base-core', ext: 'aar') 62 | implementation 'com.squareup.okhttp3:okhttp:4.9.1' //若项目中原本存在无需添加 63 | implementation 'com.google.code.gson:gson:2.8.6' //若项目中原本存在无需添加 64 | } 65 | ``` 66 | 67 | ### 注意点 68 | 1. 在 app 的 build.gradle android 域下添加如下配置 69 | ``` 70 | packagingOptions { 71 | doNotStrip "/arm64-v8a/libalive_detected.so" 72 | doNotStrip "/armeabi-v7a/libalive_detected.so" 73 | } 74 | ``` 75 | 2. 如果同时使用易盾的活体检测和身份证OCR SDK,请务必先引用OCR SDK; 遇到so冲突,请用以下方式解决 76 | 77 | ``` 78 | packagingOptions { 79 | pickFirst 'lib/arm64-v8a/libc++_shared.so' 80 | pickFirst 'lib/armeabi-v7a/libc++_shared.so' 81 | } 82 | ``` 83 | 84 | ## 各种配置 85 | 86 | ### 权限配置 87 | 88 | SDK 依赖如下权限 89 | 90 | ``` 91 | 92 | ``` 93 | 94 | 其中 CAMERA 权限是隐私权限,Android 6.0 及以上需要动态申请。使用前务必先动态申请权限 95 | 96 | ``` 97 | ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 0); 98 | ``` 99 | 100 | ### 混淆配置 101 | 102 | 在 proguard-rules.pro 文件中添加如下混淆规则 103 | 104 | ``` 105 | -keeppackagenames com.netease.nis.alivedetected 106 | -keep class com.netease.nis.alivedetected.**{*;} 107 | -dontwarn com.netease.nis.alivedetected.** 108 | -keep class com.netease.cloud.nos.yidun.**{*;} 109 | -dontwarn com.netease.cloud.nos.yidun.** 110 | ``` 111 | 112 | ## 快速调用示例 113 | 114 | ### 在 layout 布局文件中使用活体检测相机预览 View 115 | 116 | #### 注意点 117 | 118 | - 为了避免在某些中低端机型上检测卡顿,建议预览控件的宽与高不要设置为全屏,过大的预览控件会导致处理的数据过大,降低检测流畅度 119 | - 预览宽高不要随意设置,请遵守大部分相机支持的预览宽高比,3:4 或 9:16 120 | - 最好限制竖屏,横屏会影响效果 121 | 122 | #### 示例 123 | 124 | ``` 125 | 129 | ``` 130 | 131 | ### 在代码中调用相对应 api 开启活体检测 132 | 133 | ``` 134 | public class DemoActivity extends AppCompatActivity { 135 | private AliveDetector aliveDetector; 136 | 137 | @Override 138 | protected void onCreate(Bundle savedInstanceState) { 139 | super.onCreate(savedInstanceState); 140 | setContentView(R.layout.activity_demo); 141 | 142 | NISCameraPreview cameraPreview = findViewById(R.id.surface_view); 143 | aliveDetector = AliveDetector.getInstance(); 144 | aliveDetector.init(this, cameraPreview, "申请的业务id"); 145 | aliveDetector.setDetectedListener(new DetectedListener() { 146 | @Override 147 | public void onReady(boolean isInitSuccess) { 148 | 149 | } 150 | 151 | @Override 152 | public void onActionCommands(ActionType[] actionTypes) { 153 | 154 | } 155 | 156 | @Override 157 | public void onStateTipChanged(ActionType actionType, String stateTip, int code) { 158 | //单步动作 actionType.getActionID()为 0:正视前方 1:向右转头 2:向左转头 3:张嘴动作 4:眨眼动作 5:动作错误 159 | 6:动作通过 160 | } 161 | 162 | @Override 163 | public void onPassed(boolean isPassed, String token) { 164 | 165 | } 166 | 167 | @Override 168 | public void onCheck() { 169 | 170 | } 171 | 172 | @Override 173 | public void onError(int code, String msg, String token) { 174 | 175 | } 176 | 177 | @Override 178 | public void onOverTime() { 179 | 180 | } 181 | }); 182 | aliveDetector.startDetect(); 183 | } 184 | 185 | @Override 186 | protected void onDestroy() { 187 | super.onDestroy(); 188 | 189 | if (isFinishing()) { 190 | if (aliveDetector != null) { 191 | aliveDetector.stopDetect(); 192 | aliveDetector.destroy(); 193 | } 194 | } 195 | } 196 | } 197 | ``` 198 | 199 | 更多使用场景请参考 200 | [demo](https://github.com/yidun/alive-detected-android-demo) 201 | 202 | ## SDK 方法说明 203 | 204 | ### 1. 获取 AliveDetector 单例对象 205 | 206 | #### 代码说明 207 | 208 | ``` 209 | AliveDetector aliveDetector = AliveDetector.getInstance(); 210 | ``` 211 | 212 | ### 2. 初始化 213 | 214 | #### 代码说明 215 | 216 | ``` 217 | aliveDetector.init(Context context, NISCameraPreview cameraPreview, String businessId) 218 | ``` 219 | 220 | #### 参数说明 221 | 222 | |参数|类型|是否必填|默认值|描述| 223 | |----|----|--------|------|----| 224 | |context|Context|是|无| 上下文 | 225 | |cameraPreview|NISCameraPreview|是|无|相机预览 View | 226 | |businessId|String|是|无| 活体检测业务 id | 227 | 228 | ### 3. 设置回调监听 229 | 230 | #### 代码说明 231 | 232 | 代码添加在 init 之后 startDetect 之前调用 233 | 234 | ``` 235 | aliveDetector.setDetectedListener(DetectedListener detectedListener) 236 | ``` 237 | 238 | #### 参数说明 239 | 240 | |参数|类型|是否必填|默认值|描述| 241 | |----|----|--------|------|----| 242 | |detectedListener|DetectedListener|是|无| 监听接口 | 243 | 244 | #### DetectedListener 接口说明 245 | 246 | ``` 247 | public interface DetectedListener { 248 | /** 249 | * 活体检测引擎初始化时回调 250 | * 251 | * @param isInitSuccess 活体检测引擎是否初始化成功: 252 | * 1)true,初始化完成可以开始检测 253 | * 2)false,初始化失败,可尝试重新启动活体检测流程 {@link AliveDetector#startDetect()} 254 | */ 255 | void onReady(boolean isInitSuccess); 256 | 257 | /** 258 | * 此次活体检测下发的待检测动作指令序列,{@link ActionType} 259 | * 260 | * @param actionTypes 261 | */ 262 | void onActionCommands(ActionType[] actionTypes); 263 | 264 | /** 265 | * 活体检测状态是否改变,当引擎检测到状态改变时会回调该接口 266 | * 267 | * @param actionType 当前动作类型,枚举值,总共6种类型: 268 | * ACTION_STRAIGHT_AHEAD("0", "正视前方"), 269 | * ACTION_TURN_HEAD_TO_RIGHT("1", "向右转头"), 270 | * ACTION_TURN_HEAD_TO_LEFT("2", "向左转头"), 271 | * ACTION_OPEN_MOUTH("3", "张嘴动作"), 272 | * ACTION_BLINK_EYES("4", "眨眼动作"), 273 | * ACTION_ERROR("5", "动作错误"), 274 | * ACTION_PASSED("6", "动作通过") 275 | * 276 | * @param stateTip 引擎检测到的实时状态 277 | * @param code 错误码,在ACTION_ERROR时用于国际化使用(0:手机抖动,请保持稳定、1:请移动人脸到摄像头视野中间、2:环境光线暗、3:环境光线过亮、4:图像质量模糊、5:多人脸) 278 | */ 279 | void onStateTipChanged(ActionType actionType, String stateTip, int code); 280 | 281 | /** 282 | * 活体检测是否通过回调 283 | * 284 | * @param isPassed 活体检测是否通过,true:通过,false:不通过 285 | * @param token 此次活体检测返回的易盾token 286 | */ 287 | void onPassed(boolean isPassed, String token); 288 | /** 289 | * 活体检测本地检测通过 290 | * 启动远程检测 291 | */ 292 | void onCheck(); 293 | /** 294 | * 活体检测过程中出现错误时回调 295 | * 296 | * @param code 错误码 297 | * 1:sdk内部异常 2:服务端返回数据异常 3:上传图片异常 298 | * @param msg 出错原因 299 | */ 300 | void onError(int code, String msg, String token); 301 | 302 | /** 303 | * 活体检测过程超时回调 304 | */ 305 | void onOverTime(); 306 | } 307 | ``` 308 | 309 | ### 4. 开始活体检测 310 | 311 | 代码添加在 init 之后调用 312 | 313 | #### 代码说明 314 | 315 | ``` 316 | aliveDetector.startDetect() 317 | ``` 318 | 319 | ### 5. 停止活体检测 320 | 321 | #### 代码说明 322 | 323 | 可在onStop、onDestroy中调用 324 | 325 | ``` 326 | aliveDetector.stopDetect() 327 | ``` 328 | 329 | ### 6. 释放资源(建议放在 onDestroy) 330 | 331 | #### 代码说明 332 | 333 | ``` 334 | aliveDetector.destroy() 335 | ``` 336 | 337 | ### 7. 是否开启调试模式(非必须) 338 | 339 | #### 代码说明 340 | 341 | ``` 342 | aliveDetector.setDebugMode(boolean isDebug) 343 | ``` 344 | 345 | #### 参数说明 346 | 347 | |参数|类型|是否必填|默认值|描述| 348 | |----|----|--------|------|----| 349 | |isDebug|boolean|是|false| 是否打印日志 | 350 | 351 | ### 8. 设置检测动作灵敏度(非必须) 352 | 353 | #### 代码说明 354 | 355 | ``` 356 | aliveDetector.setSensitivity(int sensitivity) 357 | ``` 358 | 359 | #### 参数说明 360 | 361 | |参数|类型|是否必填|默认值|描述| 362 | |----|----|--------|------|----| 363 | |sensitivity|int|是|AliveDetector.SENSITIVITY_NORMAL| 可取值 AliveDetector.SENSITIVITY_EASY = 0、AliveDetector.SENSITIVITY_NORMAL = 1、AliveDetector.SENSITIVITY_HARD = 2 分别对应容易、普通、难 | 364 | 365 | ### 9. 设置超时时间(非必须) 366 | 367 | 代码添加在 startDetect 之前调用 368 | 369 | #### 代码说明 370 | 371 | ``` 372 | aliveDetector.setTimeOut(long timeout) 373 | ``` 374 | 375 | #### 参数说明 376 | 377 | |参数|类型|是否必填|默认值|描述| 378 | |----|----|--------|------|----| 379 | |timeout|long|是| 30000 | 单位毫秒 | 380 | 381 | ### 10. 设置域名(非必须) 382 | 383 | 用于海外域名失效的场景,可以设置私有化的域名列表,内部会自动切换 384 | 385 | #### 代码说明 386 | 387 | ``` 388 | aliveDetector.setHosts(String[] hosts) 389 | ``` 390 | 391 | #### 参数说明 392 | 393 | |参数|类型|是否必填|默认值|描述| 394 | |----|----|--------|------|----| 395 | |hosts|String[]|是| 无 | 多域名 | 396 | 397 | ### 11. 是否允许多人脸(非必须) 398 | 399 | #### 代码说明 400 | 401 | ``` 402 | aliveDetector.setAllowMultipleFace(boolean allowMultipleFace) 403 | ``` 404 | 405 | #### 参数说明 406 | 407 | |参数|类型|是否必填|默认值|描述| 408 | |----|----|--------|------|----| 409 | |allowMultipleFace|boolean|是| false | 是否允许多人脸 | 410 | 411 | ### 12. 设置设备抖动检测阈值(非必须) 412 | 413 | 值越小越灵敏 414 | 415 | #### 代码说明 416 | 417 | ``` 418 | aliveDetector.setShakeThreshold(int shakeThreshold) 419 | ``` 420 | 421 | #### 参数说明 422 | 423 | |参数|类型|是否必填|默认值|描述| 424 | |----|----|--------|------|----| 425 | |shakeThreshold|int|是| 35 | 抖动检测阈值 | 426 | 427 | ### 13. 设置人脸占比阈值(非必须) 428 | 429 | #### 代码说明 430 | 431 | ``` 432 | aliveDetector.setFaceThreshold(int faceThreshold) 433 | ``` 434 | 435 | #### 参数说明 436 | 437 | |参数|类型|是否必填| 默认值 | 描述 | 438 | |----|----|--------|-----|---------------| 439 | |faceThreshold|int|是| 7 | 人脸占比阈值,范围7-50 | 440 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 34 7 | defaultConfig { 8 | applicationId "com.netease.nis.alivedetecteddemo" 9 | minSdkVersion 21 10 | targetSdkVersion 34 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | ndk { 15 | abiFilters 'armeabi-v7a', 'arm64-v8a' 16 | } 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | kotlinOptions { 29 | jvmTarget = '1.8' 30 | } 31 | } 32 | dependencies { 33 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 34 | implementation 'androidx.appcompat:appcompat:1.4.2' 35 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 36 | testImplementation 'junit:junit:4.13.2' 37 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 38 | implementation 'io.github.yidun:livedetect:3.3.0' 39 | implementation 'com.sfyc.ctpv:library:1.1.3' 40 | implementation 'com.squareup.okhttp3:okhttp:4.9.2' 41 | implementation 'com.google.code.gson:gson:2.8.6' 42 | implementation 'pub.devrel:easypermissions:3.0.0' 43 | implementation 'com.github.bumptech.glide:glide:4.12.0' 44 | annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' 45 | implementation 'com.github.mmin18:realtimeblurview:1.2.1' 46 | 47 | implementation 'com.tencent.tbs:tbssdk:44199' 48 | } 49 | -------------------------------------------------------------------------------- /app/local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Fri May 29 19:32:10 CST 2020 8 | sdk.dir=E\:\\Android_SDK 9 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/netease/nis/alivedetecteddemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.netease.nis.alivedetecteddemo; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.netease.nis.alivedetecteddemo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 37 | 40 | 43 | 46 | 47 | 48 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/assets/blink_eyes.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yidun/alive-detected-android-demo/1f9eb853ce6649253191e448c52841fb00a860bf/app/src/main/assets/blink_eyes.wav -------------------------------------------------------------------------------- /app/src/main/assets/open_mouth.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yidun/alive-detected-android-demo/1f9eb853ce6649253191e448c52841fb00a860bf/app/src/main/assets/open_mouth.wav -------------------------------------------------------------------------------- /app/src/main/assets/pic_error_742.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yidun/alive-detected-android-demo/1f9eb853ce6649253191e448c52841fb00a860bf/app/src/main/assets/pic_error_742.zip -------------------------------------------------------------------------------- /app/src/main/assets/turn_head_to_left.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yidun/alive-detected-android-demo/1f9eb853ce6649253191e448c52841fb00a860bf/app/src/main/assets/turn_head_to_left.wav -------------------------------------------------------------------------------- /app/src/main/assets/turn_head_to_right.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yidun/alive-detected-android-demo/1f9eb853ce6649253191e448c52841fb00a860bf/app/src/main/assets/turn_head_to_right.wav -------------------------------------------------------------------------------- /app/src/main/java/com/netease/nis/alivedetecteddemo/App.kt: -------------------------------------------------------------------------------- 1 | package com.netease.nis.alivedetecteddemo 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import com.tencent.smtt.sdk.QbSdk 6 | 7 | /** 8 | * @author liuxiaoshuai 9 | * @date 2022/7/11 10 | * @desc 11 | * @email liulingfeng@mistong.com 12 | */ 13 | class App : Application() { 14 | override fun onCreate() { 15 | super.onCreate() 16 | 17 | QbSdk.initX5Environment(this, object : QbSdk.PreInitCallback { 18 | override fun onCoreInitFinished() { 19 | } 20 | 21 | override fun onViewInitFinished(isX5: Boolean) { 22 | Log.d("App", "x5预初始化结束$isX5") 23 | } 24 | 25 | }) 26 | QbSdk.setDownloadWithoutWifi(true) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netease/nis/alivedetecteddemo/FailureActivity.kt: -------------------------------------------------------------------------------- 1 | package com.netease.nis.alivedetecteddemo 2 | 3 | import android.app.Activity 4 | import android.content.ClipData 5 | import android.content.ClipboardManager 6 | import android.os.Bundle 7 | import android.text.TextUtils 8 | import android.widget.TextView 9 | import com.netease.nis.alivedetecteddemo.manager.showToast 10 | import kotlinx.android.synthetic.main.activity_failure.* 11 | 12 | /** 13 | * Created by hzhuqi on 2019/10/14 14 | */ 15 | class FailureActivity : Activity() { 16 | private var tvToken: TextView? = null 17 | private var mClipboardManager: ClipboardManager? = null 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | setContentView(R.layout.activity_failure) 21 | mClipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager 22 | val token = intent.getStringExtra("token") 23 | initView(token) 24 | } 25 | 26 | private fun initView(token: String?) { 27 | tvToken = findViewById(R.id.tv_token) 28 | if (!TextUtils.isEmpty(token)) { 29 | tvToken?.text = token 30 | } 31 | btn_back_to_demo.setOnClickListener { 32 | finish() 33 | } 34 | img_btn_back.setOnClickListener { 35 | finish() 36 | } 37 | tv_copy.setOnClickListener { 38 | val clipData = ClipData.newPlainText("token", tvToken?.text.toString()) 39 | mClipboardManager?.setPrimaryClip(clipData) 40 | "Token复制成功".showToast(this@FailureActivity) 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netease/nis/alivedetecteddemo/H5WebViewActivity.kt: -------------------------------------------------------------------------------- 1 | package com.netease.nis.alivedetecteddemo 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.util.Log 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.core.app.ActivityCompat 11 | import androidx.core.content.ContextCompat 12 | import com.tencent.smtt.export.external.TbsCoreSettings 13 | import com.tencent.smtt.export.external.interfaces.PermissionRequest 14 | import com.tencent.smtt.sdk.QbSdk 15 | import com.tencent.smtt.sdk.WebChromeClient 16 | import kotlinx.android.synthetic.main.activity_webview_h5.* 17 | 18 | 19 | /** 20 | * @author liuxiaoshuai 21 | * @date 2022/7/11 22 | * @desc 腾讯h5 webView示例 23 | * @email liulingfeng@mistong.com 24 | */ 25 | class H5WebViewActivity : AppCompatActivity() { 26 | companion object { 27 | private const val ALIVE_URL = 28 | "https://verify.dun.163.com/prod/index.html" 29 | } 30 | 31 | private var permissionRequest: PermissionRequest? = null 32 | 33 | @SuppressLint("SetJavaScriptEnabled") 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | setContentView(R.layout.activity_webview_h5) 37 | 38 | // 在调用TBS初始化、创建WebView之前进行如下配置 39 | // 在调用TBS初始化、创建WebView之前进行如下配置 40 | val map = HashMap() 41 | map[TbsCoreSettings.TBS_SETTINGS_USE_SPEEDY_CLASSLOADER] = true 42 | map[TbsCoreSettings.TBS_SETTINGS_USE_DEXLOADER_SERVICE] = true 43 | QbSdk.initTbsSettings(map) 44 | 45 | // 刘海屏适配 46 | webView?.settingsExtension?.setDisplayCutoutEnable(true) 47 | webView?.settings?.savePassword = false 48 | webView?.settings?.javaScriptEnabled = true 49 | webView?.webChromeClient = object : WebChromeClient() { 50 | override fun onPermissionRequest(request: PermissionRequest?) { 51 | Log.i("H5WebViewActivity", "h5权限回调") 52 | if (ContextCompat.checkSelfPermission( 53 | this@H5WebViewActivity, 54 | Manifest.permission.CAMERA 55 | ) != PackageManager.PERMISSION_GRANTED 56 | ) { 57 | Log.i("H5WebViewActivity", "权限申请") 58 | this@H5WebViewActivity.permissionRequest = request 59 | ActivityCompat.requestPermissions( 60 | this@H5WebViewActivity, 61 | arrayOf(Manifest.permission.CAMERA), 62 | 1001 63 | ) 64 | } else { 65 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 66 | request?.grant(request.resources) 67 | } 68 | } 69 | } 70 | } 71 | webView?.loadUrl(ALIVE_URL) 72 | } 73 | 74 | override fun onDestroy() { 75 | try { 76 | if (webView != null) { 77 | webView.stopLoading() 78 | webView.removeAllViewsInLayout() 79 | webView.removeAllViews() 80 | webView.webViewClient = null 81 | webView.destroy() 82 | } 83 | } catch (throwable: Throwable) { 84 | throwable.printStackTrace(); 85 | } finally { 86 | super.onDestroy(); 87 | } 88 | } 89 | 90 | override fun onRequestPermissionsResult( 91 | requestCode: Int, 92 | permissions: Array, 93 | grantResults: IntArray 94 | ) { 95 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 96 | if (requestCode == 1001 && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 97 | Log.i("WebViewActivity", "权限申请通过") 98 | permissionRequest?.grant(permissionRequest?.resources) 99 | } else { 100 | Log.i("WebViewActivity", "权限申请拒绝") 101 | permissionRequest?.deny() 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netease/nis/alivedetecteddemo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.netease.nis.alivedetecteddemo 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.ProgressDialog 5 | import android.content.Intent 6 | import android.content.res.AssetFileDescriptor 7 | import android.media.MediaPlayer 8 | import android.os.Bundle 9 | import android.text.TextUtils 10 | import android.util.Log 11 | import android.view.LayoutInflater 12 | import android.view.View 13 | import android.view.WindowManager 14 | import android.widget.LinearLayout 15 | import android.widget.TextView 16 | import androidx.appcompat.app.AppCompatActivity 17 | import com.bumptech.glide.Glide 18 | import com.netease.nis.alivedetected.ActionType 19 | import com.netease.nis.alivedetected.AliveDetector 20 | import com.netease.nis.alivedetected.DetectedListener 21 | import com.netease.nis.alivedetecteddemo.manager.BroadcastDispatcher 22 | import com.netease.nis.alivedetecteddemo.utils.Util 23 | import kotlinx.android.synthetic.main.activity_main.* 24 | import java.io.IOException 25 | import java.util.* 26 | 27 | /** 28 | * @author liu 29 | * @date 2021/10/12 30 | * @desc 31 | * @email liulingfeng@mistong.com 32 | */ 33 | class MainActivity : AppCompatActivity() { 34 | companion object { 35 | private const val TAG = "MainActivity" 36 | } 37 | 38 | private var mAliveDetector: AliveDetector? = null 39 | private var mActions: Array? = null 40 | private var mCurrentCheckStepIndex = 0 41 | private var mCurrentActionType = ActionType.ACTION_STRAIGHT_AHEAD 42 | private var llStep: LinearLayout? = null 43 | private var isOpenVoice = true 44 | private var mPlayer: MediaPlayer? = null 45 | private var progressDialog: ProgressDialog? = null 46 | 47 | override fun onCreate(savedInstanceState: Bundle?) { 48 | super.onCreate(savedInstanceState) 49 | Util.setWindowBrightness(this, WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL) 50 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 51 | setContentView(R.layout.activity_main) 52 | BroadcastDispatcher.registerScreenOff(this) 53 | 54 | initView() 55 | } 56 | 57 | private fun initView() { 58 | mPlayer = MediaPlayer() 59 | progressDialog = ProgressDialog(this) 60 | progressDialog?.setTitle("云端检测中") 61 | 62 | llStep = findViewById(R.id.ll_step) 63 | img_btn_back?.setOnClickListener { 64 | mAliveDetector?.stopDetect() 65 | finish() 66 | } 67 | 68 | iv_voice?.setOnClickListener { 69 | isOpenVoice = !isOpenVoice 70 | if (isOpenVoice) { 71 | iv_voice?.setImageResource(R.mipmap.ico_voice_open_2x) 72 | } else { 73 | iv_voice?.setImageResource(R.mipmap.ico_voice_close_2x) 74 | } 75 | } 76 | 77 | BroadcastDispatcher.addScreenStatusChangedListener(object : 78 | BroadcastDispatcher.ScreenStatusChangedListener { 79 | override fun onForeground() { 80 | resetIndicator() 81 | resetGif() 82 | mAliveDetector?.startDetect() 83 | } 84 | 85 | override fun onBackground() { 86 | mAliveDetector?.stopDetect() 87 | } 88 | 89 | }) 90 | 91 | initData() 92 | } 93 | 94 | private fun initData() { 95 | mAliveDetector = AliveDetector.getInstance() 96 | mAliveDetector?.setDebugMode(true) 97 | mAliveDetector?.init(this, surface_view, "易盾业务id") 98 | mAliveDetector?.setDetectedListener(object : DetectedListener { 99 | override fun onReady(isInitSuccess: Boolean) { 100 | // 开始倒计时 101 | pv_count_time?.startCountTimeAnimation() 102 | // 引擎初始化完成 103 | if (isInitSuccess) Log.d(TAG, "活体检测引擎初始化完成") else Log.e( 104 | TAG, 105 | "活体检测引擎初始化失败" 106 | ) 107 | } 108 | 109 | override fun onActionCommands(actionTypes: Array?) { 110 | // 此次活体检测下发的待检测动作指令序列 111 | mActions = actionTypes 112 | val commands = buildActionCommand(actionTypes) 113 | Log.d(TAG, "活体检测动作序列为:$commands") 114 | showIndicatorOnUiThread(commands.length - 1) 115 | } 116 | 117 | override fun onStateTipChanged(actionType: ActionType?, stateTip: String?, code: Int) { 118 | // 单步动作 119 | Log.d( 120 | TAG, 121 | "actionType:" + actionType?.actionTip + " stateTip:" + stateTip + " CurrentCheckStepIndex:" + mCurrentCheckStepIndex 122 | ) 123 | when (actionType) { 124 | ActionType.ACTION_ERROR -> setTipText(stateTip, true) 125 | ActionType.ACTION_PASSED -> { 126 | Log.d(TAG, "检测通过") 127 | } 128 | 129 | else -> setTipText(stateTip, false) 130 | } 131 | 132 | if (actionType == ActionType.ACTION_PASSED && actionType.actionID != mCurrentActionType.actionID) { 133 | mCurrentCheckStepIndex++ 134 | mActions?.let { 135 | if (mCurrentCheckStepIndex < it.size) { 136 | updateIndicatorOnUiThread(mCurrentCheckStepIndex) 137 | if (isOpenVoice) { 138 | playSounds(mCurrentCheckStepIndex) 139 | } 140 | mCurrentActionType = it[mCurrentCheckStepIndex] 141 | } 142 | } 143 | 144 | } 145 | } 146 | 147 | override fun onPassed(isPassed: Boolean, token: String?) { 148 | // 检测通过 149 | if (progressDialog?.isShowing == true) { 150 | progressDialog?.dismiss() 151 | } 152 | if (isPassed) { 153 | Log.d(TAG, "活体检测通过,token is:$token") 154 | finish() 155 | val intent = Intent(this@MainActivity, SuccessActivity::class.java) 156 | startActivity(intent) 157 | } else { 158 | Log.e(TAG, "活体检测不通过,token is:$token") 159 | finish() 160 | val intent = Intent( 161 | this@MainActivity, 162 | FailureActivity::class.java 163 | ) 164 | intent.putExtra("token", token) 165 | startActivity(intent) 166 | } 167 | } 168 | 169 | override fun onCheck() { 170 | if (!isFinishing) { 171 | progressDialog?.show() 172 | } 173 | } 174 | 175 | override fun onError(code: Int, msg: String?, token: String?) { 176 | if (progressDialog?.isShowing == true) { 177 | progressDialog?.dismiss() 178 | } 179 | Log.e(TAG, "listener [onError] 活体检测出错,原因:$msg token:$token") 180 | } 181 | 182 | override fun onOverTime() { 183 | Util.showDialog(this@MainActivity, "检测超时", "请在规定时间内完成动作", 184 | "重试", "返回首页", { _, _ -> 185 | resetIndicator() 186 | resetGif() 187 | mAliveDetector?.startDetect() 188 | }) { _, _ -> 189 | val intent = Intent( 190 | this@MainActivity, 191 | WelcomeActivity::class.java 192 | ) 193 | startActivity(intent) 194 | } 195 | } 196 | 197 | }) 198 | 199 | mAliveDetector?.sensitivity = AliveDetector.SENSITIVITY_NORMAL 200 | mAliveDetector?.setTimeOut(30000) 201 | mAliveDetector?.startDetect() 202 | } 203 | 204 | /** 205 | * actionIds 206 | */ 207 | private fun buildActionCommand(actionCommands: Array?): String { 208 | val commands = StringBuilder() 209 | actionCommands?.let { 210 | for (actionType in it) { 211 | commands.append(actionType.actionID) 212 | } 213 | } 214 | return if (TextUtils.isEmpty(commands.toString())) "" else commands.toString() 215 | } 216 | 217 | // 显示所有步骤 218 | private fun showIndicatorOnUiThread(commandLength: Int) { 219 | llStep?.removeAllViews() 220 | for (index in 0 until commandLength) { 221 | val tvStep = 222 | LayoutInflater.from(this).inflate(R.layout.layout_tv_step, null) as TextView 223 | if (index == 0) { 224 | tvStep.text = "1" 225 | setTextViewFocus(tvStep) 226 | } 227 | llStep?.addView(tvStep) 228 | val param: LinearLayout.LayoutParams = tvStep.layoutParams as LinearLayout.LayoutParams 229 | param.width = Util.dip2px(this, 18.0f) 230 | param.height = Util.dip2px(this, 18.0f) 231 | param.leftMargin = Util.dip2px(this, 5.0f) 232 | tvStep.layoutParams = param 233 | } 234 | } 235 | 236 | private fun updateIndicatorOnUiThread(currentActionIndex: Int) { 237 | updateIndicator(currentActionIndex) 238 | updateGif(currentActionIndex) 239 | } 240 | 241 | private fun updateIndicator(currentActionPassedCount: Int) { 242 | llStep?.let { 243 | if (currentActionPassedCount > 0 && currentActionPassedCount <= it.childCount) { 244 | val tv = llStep?.getChildAt(currentActionPassedCount - 1) as TextView 245 | if (currentActionPassedCount > 1) { 246 | val lastTv = llStep?.getChildAt(currentActionPassedCount - 2) as TextView 247 | setTextViewUnFocus(lastTv) 248 | } 249 | tv.text = currentActionPassedCount.toString() 250 | setTextViewFocus(tv) 251 | } 252 | } 253 | } 254 | 255 | private fun updateGif(currentActionIndex: Int) { 256 | mActions?.let { 257 | when (it[currentActionIndex]) { 258 | ActionType.ACTION_TURN_HEAD_TO_LEFT -> { 259 | gif_action?.let { 260 | Glide.with(this).asGif().load(R.drawable.turn_left).into(gif_action) 261 | } 262 | } 263 | 264 | ActionType.ACTION_TURN_HEAD_TO_RIGHT -> { 265 | gif_action?.let { 266 | Glide.with(this).asGif().load(R.drawable.turn_right).into(gif_action) 267 | } 268 | } 269 | 270 | ActionType.ACTION_OPEN_MOUTH -> { 271 | gif_action?.let { 272 | Glide.with(this).asGif().load(R.drawable.open_mouth).into(gif_action) 273 | } 274 | } 275 | 276 | ActionType.ACTION_BLINK_EYES -> { 277 | gif_action?.let { 278 | Glide.with(this).asGif().load(R.drawable.open_eyes).into(gif_action) 279 | } 280 | } 281 | 282 | else -> { 283 | Log.d(TAG, "不支持的类型") 284 | } 285 | } 286 | } 287 | } 288 | 289 | private fun resetIndicator() { 290 | mCurrentCheckStepIndex = 0 291 | mCurrentActionType = ActionType.ACTION_STRAIGHT_AHEAD 292 | } 293 | 294 | private fun resetGif() { 295 | gif_action?.let { 296 | Glide.with(this).load(R.mipmap.pic_front_2x).into(it) 297 | } 298 | } 299 | 300 | private fun setTextViewFocus(tv: TextView?) { 301 | tv?.setBackgroundResource(R.drawable.circle_tv_focus) 302 | } 303 | 304 | private fun setTextViewUnFocus(tv: TextView?) { 305 | tv?.text = "" 306 | tv?.setBackgroundResource(R.drawable.circle_tv_un_focus) 307 | } 308 | 309 | private fun playSounds(currentActionIndex: Int) { 310 | mActions?.let { 311 | when (it[currentActionIndex]) { 312 | ActionType.ACTION_TURN_HEAD_TO_LEFT -> playSound(getAssetFileDescriptor("turn_head_to_left.wav")) 313 | ActionType.ACTION_TURN_HEAD_TO_RIGHT -> playSound(getAssetFileDescriptor("turn_head_to_right.wav")) 314 | ActionType.ACTION_OPEN_MOUTH -> playSound(getAssetFileDescriptor("open_mouth.wav")) 315 | ActionType.ACTION_BLINK_EYES -> playSound(getAssetFileDescriptor("blink_eyes.wav")) 316 | else -> { 317 | Log.d(TAG, "不支持的类型") 318 | } 319 | } 320 | } 321 | } 322 | 323 | private fun playSound(fileDescriptor: AssetFileDescriptor?) { 324 | try { 325 | mPlayer?.reset() 326 | fileDescriptor?.let { 327 | mPlayer?.setDataSource( 328 | it.fileDescriptor, 329 | it.startOffset, 330 | it.length 331 | ) 332 | } 333 | mPlayer?.prepare() 334 | mPlayer?.start() 335 | } catch (e: IOException) { 336 | e.printStackTrace() 337 | Log.e(TAG, "playSound error$e") 338 | } 339 | } 340 | 341 | private fun getAssetFileDescriptor(assetName: String): AssetFileDescriptor? { 342 | try { 343 | return application.assets.openFd(assetName) 344 | } catch (e: IOException) { 345 | e.printStackTrace() 346 | Log.e(TAG, "getAssetFileDescriptor error$e") 347 | } 348 | return null 349 | } 350 | 351 | @SuppressLint("SetTextI18n") 352 | private fun setTipText(tip: String?, isErrorType: Boolean) { 353 | if (isErrorType) { 354 | when (tip) { 355 | "请移动人脸到摄像头视野中间" -> tv_error_tip?.text = 356 | "请正对手机屏幕\n将面部移入框内" 357 | 358 | "请正视摄像头视野中间并保持不动" -> tv_error_tip?.text = "请正视摄像头\n并保持不动" 359 | else -> tv_error_tip?.text = tip 360 | } 361 | view_tip_background?.visibility = View.VISIBLE 362 | blur_view?.visibility = View.VISIBLE 363 | } else { 364 | view_tip_background?.visibility = View.INVISIBLE 365 | blur_view?.visibility = View.INVISIBLE 366 | tv_tip?.text = tip 367 | tv_error_tip?.text = "" 368 | } 369 | } 370 | 371 | override fun onPause() { 372 | super.onPause() 373 | 374 | progressDialog?.let { 375 | if (it.isShowing) { 376 | it.dismiss() 377 | } 378 | } 379 | } 380 | 381 | override fun onDestroy() { 382 | Util.setWindowBrightness(this, WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) 383 | if (isFinishing) { 384 | mAliveDetector?.stopDetect() 385 | mAliveDetector?.destroy() 386 | pv_count_time?.cancelCountTimeAnimation() 387 | BroadcastDispatcher.unRegisterScreenOff(this) 388 | } 389 | 390 | if (mPlayer?.isPlaying == true) { 391 | mPlayer?.stop() 392 | } 393 | mPlayer?.reset() 394 | mPlayer?.release() 395 | mPlayer = null 396 | super.onDestroy() 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /app/src/main/java/com/netease/nis/alivedetecteddemo/SuccessActivity.kt: -------------------------------------------------------------------------------- 1 | package com.netease.nis.alivedetecteddemo 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import kotlinx.android.synthetic.main.activity_success.* 6 | 7 | /** 8 | * Created by hzhuqi on 2019/10/14 9 | */ 10 | class SuccessActivity : AppCompatActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_success) 14 | initView() 15 | } 16 | 17 | private fun initView() { 18 | btn_back_to_demo.setOnClickListener { 19 | finish() 20 | } 21 | img_btn_back.setOnClickListener { 22 | finish() 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netease/nis/alivedetecteddemo/WebViewActivity.kt: -------------------------------------------------------------------------------- 1 | package com.netease.nis.alivedetecteddemo 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.content.pm.PackageManager 6 | import android.graphics.Bitmap 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.util.Log 10 | import android.view.KeyEvent 11 | import android.view.ViewGroup 12 | import android.view.ViewParent 13 | import android.webkit.JsPromptResult 14 | import android.webkit.PermissionRequest 15 | import android.webkit.WebChromeClient 16 | import android.webkit.WebResourceRequest 17 | import android.webkit.WebView 18 | import android.webkit.WebViewClient 19 | import androidx.annotation.RequiresApi 20 | import androidx.appcompat.app.AppCompatActivity 21 | import androidx.core.app.ActivityCompat 22 | import androidx.core.content.ContextCompat 23 | 24 | /** 25 | * @author liu 26 | * @date 2021/10/12 27 | * @desc H5视频活体 28 | * @email liulingfeng@mistong.com 29 | */ 30 | class WebViewActivity : AppCompatActivity() { 31 | companion object { 32 | private const val ALIVE_URL = 33 | "https://verify.dun.163.com/prod/index.html" 34 | } 35 | 36 | private var permissionRequest: PermissionRequest? = null 37 | private var webView: WebView? = null 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | setContentView(R.layout.activity_webview) 42 | 43 | initWebView() 44 | } 45 | 46 | @SuppressLint("SetJavaScriptEnabled") 47 | private fun initWebView() { 48 | webView = findViewById(R.id.webView) 49 | webView?.settings?.javaScriptEnabled = true 50 | webView?.settings?.allowFileAccess = false 51 | webView?.settings?.setGeolocationEnabled(false) 52 | webView?.settings?.allowContentAccess = false 53 | // 缩放按钮 54 | webView?.settings?.builtInZoomControls = false 55 | // 自适应屏幕 56 | webView?.settings?.useWideViewPort = true 57 | // 默认大视野模式 58 | webView?.settings?.loadWithOverviewMode = true 59 | webView?.settings?.setSupportMultipleWindows(true) 60 | webView?.settings?.domStorageEnabled = true 61 | webView?.settings?.databaseEnabled = true 62 | webView?.settings?.defaultTextEncodingName = "UTF-8" 63 | webView?.isHorizontalScrollBarEnabled = false 64 | webView?.isVerticalScrollBarEnabled = false 65 | 66 | webView?.webViewClient = object : WebViewClient() { 67 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 68 | override fun shouldOverrideUrlLoading( 69 | view: WebView?, 70 | request: WebResourceRequest? 71 | ): Boolean { 72 | return this.shouldOverrideUrlLoading(view, request?.url.toString()) 73 | } 74 | 75 | override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { 76 | Log.i("WebViewActivity", "shouldOverrideUrlLoading${url}") 77 | return false 78 | } 79 | 80 | override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { 81 | Log.i("WebViewActivity", "onPageStarted${url}") 82 | super.onPageStarted(view, url, favicon) 83 | } 84 | 85 | override fun onPageFinished(view: WebView?, url: String?) { 86 | Log.i("WebViewActivity", "onPageFinished${url}") 87 | super.onPageFinished(view, url) 88 | } 89 | 90 | } 91 | webView?.webChromeClient = object : WebChromeClient() { 92 | override fun onJsPrompt( 93 | view: WebView?, 94 | url: String?, 95 | message: String?, 96 | defaultValue: String?, 97 | result: JsPromptResult? 98 | ): Boolean { 99 | Log.i("WebViewActivity", "onJsPrompt$message") 100 | return super.onJsPrompt(view, url, message, defaultValue, result) 101 | } 102 | 103 | override fun onPermissionRequest(request: PermissionRequest?) { 104 | Log.i("WebViewActivity", "h5权限回调") 105 | if (ContextCompat.checkSelfPermission( 106 | this@WebViewActivity, 107 | Manifest.permission.CAMERA 108 | ) != PackageManager.PERMISSION_GRANTED 109 | ) { 110 | Log.i("H5WebViewActivity", "权限申请") 111 | this@WebViewActivity.permissionRequest = request 112 | ActivityCompat.requestPermissions( 113 | this@WebViewActivity, 114 | arrayOf(Manifest.permission.CAMERA), 115 | 1001 116 | ) 117 | } else { 118 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 119 | request?.grant(request.resources) 120 | } 121 | } 122 | } 123 | } 124 | webView?.loadUrl(ALIVE_URL) 125 | } 126 | 127 | override fun onDestroy() { 128 | webView?.let { 129 | val parent: ViewParent = it.parent 130 | (parent as ViewGroup).removeView(webView) 131 | it.stopLoading() 132 | // 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错 133 | it.settings.javaScriptEnabled = false 134 | it.clearHistory() 135 | it.removeAllViews() 136 | it.destroy() 137 | } 138 | super.onDestroy() 139 | } 140 | 141 | override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { 142 | if ((keyCode == KeyEvent.KEYCODE_BACK && webView != null && webView?.canGoBack() == true)) { 143 | webView?.goBack() 144 | return true 145 | } 146 | return super.onKeyDown(keyCode, event) 147 | } 148 | 149 | override fun onRequestPermissionsResult( 150 | requestCode: Int, 151 | permissions: Array, 152 | grantResults: IntArray 153 | ) { 154 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 155 | if (requestCode == 1001 && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 156 | Log.i("WebViewActivity", "权限申请通过") 157 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 158 | permissionRequest?.grant(permissionRequest?.resources) 159 | } 160 | 161 | } else { 162 | Log.i("WebViewActivity", "权限申请拒绝") 163 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 164 | permissionRequest?.deny() 165 | } 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netease/nis/alivedetecteddemo/WelcomeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.netease.nis.alivedetecteddemo 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.widget.Toast 8 | import androidx.appcompat.app.AppCompatActivity 9 | import kotlinx.android.synthetic.main.activity_welcome.* 10 | import pub.devrel.easypermissions.AfterPermissionGranted 11 | import pub.devrel.easypermissions.EasyPermissions 12 | 13 | /** 14 | * @author liu 15 | * @date 2021/10/12 16 | * @desc 欢迎页 17 | * @email liulingfeng@mistong.com 18 | */ 19 | class WelcomeActivity : AppCompatActivity(), EasyPermissions.PermissionCallbacks { 20 | companion object { 21 | private const val TAG = "WelcomeActivity" 22 | private const val RC_ALL_PERM = 10000 23 | } 24 | 25 | private val PERMISSIONS = 26 | arrayOf(Manifest.permission.CAMERA) 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.activity_welcome) 31 | 32 | btn_jump_to_main_act.setOnClickListener { 33 | checkPermissionAndJump(MainActivity::class.java) 34 | } 35 | 36 | btn_type.setOnClickListener { 37 | checkPermissionAndJump(WebViewActivity::class.java) 38 | // 腾讯X5浏览器 39 | // checkPermissionAndJump(H5WebViewActivity::class.java) 40 | } 41 | } 42 | 43 | private fun checkPermissionAndJump(clazz: Class<*>) { 44 | if (!EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA)) { 45 | Toast.makeText(applicationContext, "您未授予相机权限,请到设置中开启权限", Toast.LENGTH_LONG).show() 46 | } else { 47 | val intent = Intent(this, clazz) 48 | startActivity(intent) 49 | } 50 | } 51 | 52 | override fun onStart() { 53 | super.onStart() 54 | requestPermissions() 55 | } 56 | 57 | override fun onRequestPermissionsResult( 58 | requestCode: Int, 59 | permissions: Array, 60 | grantResults: IntArray 61 | ) { 62 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 63 | EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) 64 | } 65 | 66 | override fun onPermissionsGranted(requestCode: Int, perms: MutableList) { 67 | Log.i(TAG, "权限申请成功") 68 | } 69 | 70 | override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { 71 | Log.i(TAG, "权限申请被拒绝") 72 | } 73 | 74 | @AfterPermissionGranted(RC_ALL_PERM) 75 | fun requestPermissions() { 76 | if (!EasyPermissions.hasPermissions( 77 | this, 78 | *PERMISSIONS 79 | ) 80 | ) { 81 | EasyPermissions.requestPermissions( 82 | this, getString(R.string.permission_tip), 83 | RC_ALL_PERM, *PERMISSIONS 84 | ) 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netease/nis/alivedetecteddemo/manager/BroadcastDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.netease.nis.alivedetecteddemo.manager 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import java.util.concurrent.atomic.AtomicBoolean 8 | 9 | /** 10 | * @author liuxiaoshuai 11 | * @date 2022/3/16 12 | * @desc 13 | * @email liulingfeng@mistong.com 14 | */ 15 | object BroadcastDispatcher { 16 | private var canInit = AtomicBoolean(true) 17 | private var isScreenOff = AtomicBoolean(false) 18 | private var screenOffReceiver: ScreenOffReceiver = ScreenOffReceiver() 19 | private lateinit var screenOffFilter: IntentFilter 20 | private var listener: ScreenStatusChangedListener? = null 21 | 22 | init { 23 | if (canInit.get()) { 24 | val intentFilter = IntentFilter("android.intent.action.SCREEN_OFF") 25 | screenOffFilter = intentFilter 26 | intentFilter.addAction("android.intent.action.SCREEN_ON") 27 | screenOffFilter.addAction("android.intent.action.USER_PRESENT") 28 | canInit.set(false) 29 | } 30 | 31 | } 32 | 33 | @Synchronized 34 | fun registerScreenOff(context: Context) { 35 | if (!isScreenOff.get()) { 36 | try { 37 | context.registerReceiver(screenOffReceiver, screenOffFilter) 38 | } catch (e: Exception) { 39 | e.printStackTrace() 40 | } 41 | isScreenOff.set(true) 42 | } 43 | } 44 | 45 | @Synchronized 46 | fun unRegisterScreenOff(context: Context) { 47 | if (isScreenOff.get()) { 48 | try { 49 | context.unregisterReceiver(screenOffReceiver) 50 | } catch (e: Exception) { 51 | e.printStackTrace() 52 | } 53 | listener = null 54 | isScreenOff.set(false) 55 | } 56 | } 57 | 58 | @Synchronized 59 | fun addScreenStatusChangedListener(listener: ScreenStatusChangedListener) { 60 | this.listener = listener 61 | } 62 | 63 | private class ScreenOffReceiver : BroadcastReceiver() { 64 | override fun onReceive(context: Context?, intent: Intent?) { 65 | val action: String? = intent?.action 66 | action?.let { 67 | when (it) { 68 | "android.intent.action.SCREEN_OFF" -> { 69 | listener?.onBackground() 70 | } 71 | // USER_PRESENT可能比SCREEN_OFF接收快 72 | "android.intent.action.USER_PRESENT" -> { 73 | listener?.onForeground() 74 | } 75 | else -> { 76 | 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | interface ScreenStatusChangedListener { 84 | fun onForeground() 85 | fun onBackground() 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netease/nis/alivedetecteddemo/manager/Toast.kt: -------------------------------------------------------------------------------- 1 | package com.netease.nis.alivedetecteddemo.manager 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | 6 | /** 7 | * @author liuxiaoshuai 8 | * @date 2022/3/16 9 | * @desc 10 | * @email liulingfeng@mistong.com 11 | */ 12 | 13 | fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) { 14 | Toast.makeText(context, this, duration).show() 15 | } 16 | 17 | fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) { 18 | Toast.makeText(context, this, duration).show() 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netease/nis/alivedetecteddemo/utils/Util.kt: -------------------------------------------------------------------------------- 1 | package com.netease.nis.alivedetecteddemo.utils 2 | 3 | import android.app.Activity 4 | import android.app.AlertDialog 5 | import android.content.Context 6 | import android.content.DialogInterface 7 | import android.net.ConnectivityManager 8 | import android.widget.Toast 9 | 10 | /** 11 | * Created by hzhuqi on 2020/2/24 12 | */ 13 | object Util { 14 | @JvmStatic 15 | fun showDialog( 16 | activity: Activity, title: String?, message: String?, 17 | positiveText: String?, negativeText: String?, 18 | positiveListener: DialogInterface.OnClickListener?, 19 | negativeListener: DialogInterface.OnClickListener? 20 | ) { 21 | if (!activity.isFinishing) { 22 | activity.runOnUiThread { 23 | val builder = AlertDialog.Builder(activity) 24 | builder.setTitle(title) 25 | .setMessage(message) 26 | .setPositiveButton(positiveText, positiveListener) 27 | .setNegativeButton(negativeText, negativeListener) 28 | .show() 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * 设置当前窗口亮度 35 | * 36 | * @param brightness 37 | */ 38 | @JvmStatic 39 | fun setWindowBrightness(context: Activity, brightness: Float) { 40 | val window = context.window 41 | val lp = window.attributes 42 | lp.screenBrightness = brightness 43 | window.attributes = lp 44 | } 45 | 46 | /** 47 | * dip转px 48 | */ 49 | fun dip2px(context: Context, dipValue: Float): Int { 50 | val scale = context.resources.displayMetrics.density 51 | return (dipValue * scale + 0.5f).toInt() 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_shape_success.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_start_alive_detected.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_tv_focus.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_tv_un_focus.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ll_shape_copy_token.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/open_eyes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yidun/alive-detected-android-demo/1f9eb853ce6649253191e448c52841fb00a860bf/app/src/main/res/drawable/open_eyes.gif -------------------------------------------------------------------------------- /app/src/main/res/drawable/open_mouth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yidun/alive-detected-android-demo/1f9eb853ce6649253191e448c52841fb00a860bf/app/src/main/res/drawable/open_mouth.gif -------------------------------------------------------------------------------- /app/src/main/res/drawable/turn_left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yidun/alive-detected-android-demo/1f9eb853ce6649253191e448c52841fb00a860bf/app/src/main/res/drawable/turn_left.gif -------------------------------------------------------------------------------- /app/src/main/res/drawable/turn_right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yidun/alive-detected-android-demo/1f9eb853ce6649253191e448c52841fb00a860bf/app/src/main/res/drawable/turn_right.gif -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_failure.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 22 | 23 | 31 | 32 | 40 | 41 | 42 | 51 | 52 | 59 | 60 |