1. WebView内嵌的H5认证页面不支持实时动态检测,或实时动态检测时唤起相机黑屏、无法预览拍摄画面
iOS
WKWebView 从 iOS14.3 开始支持 getUserMedia() 的调用,但是 WKWebView.configuration 的属性 allowsInlineMediaPlayback 默认值是 NO,该属性决定了H5是否可以播放内联HTML5视频。需要将 allowsInlineMediaPlayback 属性的值置为 YES ,即可正常展示活体检测预览。注:如果使用SFSafariViewController加载H5页面,则不存在该问题。
// 需要先初始化WKWebViewConfiguration并修改好参数,再初始化WKWebView并传入configuration,否则配置可能会不生效! WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; config.allowsInlineMediaPlayback = YES; // 默认是NO,这个值决定了用内嵌HTML5播放视频还是用本地的全屏控制 if (@available(iOS 10.0, *)) { config.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; // 音视频的播放不需要用户手势触发,即为自动播放 } else { config.requiresUserActionForMediaPlayback = NO; } // 初始化WKWebViewConfiguration后,再初始化WKWebView并传入config WKWebView *webview = [[WKWebView alloc] initWithFrame:frame configuration:config];
Android
为了使腾讯云/旷世的实时动态检测能够正常生效,同时适配可能出现的实时动态检测降级到录制视频的场景,请参考如下代码进行兼容性配置,从而确保能够正常唤起相机。
进行实时动态检测时,如遇到相机加载异常,请确保应用已经获取相机权限,或者通过监听 onPermissionRequest() 动态申请权限,该方法会在即将进行实时动态检测之前触发。
// 监听WebChromeClient中onPermissionRequest(),判断当前是否授权相机权限 @Override public void onPermissionRequest(PermissionRequest request) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { if (request != null && request.getOrigin() != null && QYSH5FaceVerifySDK.getInstance().isQiyuesuoFaceVerify(request.getOrigin().toString())) { this.request = request; ActivityCompat.requestPermissions(mActivity, new String[]{ Manifest.permission.CAMERA,}, REQUEST_CODE_CAMERA_WEBRTC); } } } // 以下为部分参考代码,完整文件见下方附件 // 设置 setWebChromeClient qysWebChromeClient = new QYSWebChromeClient(this.mWebview); mWebview.setWebChromeClient(qysWebChromeClient); mWebview.setWebViewClient(new WebViewClient()); @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); qysWebChromeClient.onActivityResult(requestCode,resultCode,data); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); qysWebChromeClient.onRequestPermissionsResult(requestCode,permissions,grantResults); } // 详细示例代码下载地址:https://dl.qiyuesuo.com/public/app/doc/webRTC-demo.zip
2. WebView内嵌的H5页面无法正常的调用相机、相册
iOS
根据苹果商店的审核条款,需要在 info.plist 中添加请求相关权限时的提示文案,否则可能会出现闪退的情况。目前契约锁的H5中可能使用到的权限包括「相机」「相册」「麦克风」。配置项如下(已配置项无需再次配置):
Android
H5页面在Android原生WebView中唤起系统相机或相册,在部分机型上可能会出现响应错误或者不响应的问题,可以通过重写 WebChromeClient->onShowFileChooser() ,根据 fileChooserParams.getAcceptTypes() 中的操作类型打开Android原生相机,相册。Android 6.0以上打开相机、相册前需要动态申请文件读取权限。
// step1 Android 7.0以上文件获取Uri 配置fileprovider QYSFileProviderUtils.initAuthorityId(BuildConfig.APPLICATION_ID + ".qysdemo.fileprovider"); // step2 可以参考Demo中QYSWebChromeClient中实现 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) { // 根据fileChooserParams.getAcceptTypes() 判断是需要唤起 相机/相机/文件/录制视频 // 根据对应需要唤起的操作判断是否需要动态申请权限,如相机拍照后保存路径设置外部路径,需要读写权限 // 有权限之后App通过Intent唤起本地相机/相机/文件/录制视频 // 例如拍照完成后,获取到文件的uri,通过filePathCallback回传,filePathCallback.onReceiveValue(new Uri[]{uri}); boolean hasPermission = PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(mActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE); if (hasPermission){ mUploadHandler = new QYSUploadHandler(mActivity,filePathCallback,fileChooserParams); }else { QYSWebChromeClient.this.filePathCallback = filePathCallback; QYSWebChromeClient.this.fileChooserParams = fileChooserParams; ActivityCompat.requestPermissions(mActivity, new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_FILE_CHOOSER); } return true; } // step3 动态申请权限操作 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); qysWebChromeClient.onRequestPermissionsResult(requestCode,permissions,grantResults); } // step4 执行选择文件/拍照后上传操作 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); qysWebChromeClient.onActivityResult(requestCode,resultCode,data); } // 备注:以上代码仅供参考,请根据自己的业务场景进行具体实现,详细示例代码下载地址:https://dl.qiyuesuo.com/public/app/doc/filecamera-demo.zip
3. WebView内嵌的H5页面无法跳转到微信、支付宝等第三方APP
在没有对WebView做任何处理的情况下,无法主动唤起微信、支付宝等第三方APP。在WebView中,可以通过拦截支付宝的认证链接,以打开URL Scheme的方式唤醒第三方APP。
iOS
在info.plist,把需要跳转的URL Scheme添加到白名单中;
实现WKWebView中的代理方法
#pragma mark - <WKNavigationDelegate> - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSURL *url = navigationAction.request.URL; NSString *urlString = url.absoluteString; if (urlString && urlString.length) { if (([urlString hasPrefix:@"weixin://"] || [urlString hasPrefix:@"alipays://"]) && [[UIApplication sharedApplication] canOpenURL:url]) { // 第三方APP的URL Scheme if (@available(iOS 10.0, *)) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; } else { [[UIApplication sharedApplication] openURL:url]; } decisionHandler(WKNavigationActionPolicyCancel); return; } } decisionHandler(WKNavigationActionPolicyAllow); }
Android
在没有对WebView做任何处理的情况下,无法主动唤起微信、支付宝等第三方APP。在WebView中,可以重写 WebViewClient 的 shouldOverrideUrlLoading 方法,以打开URL Scheme的方式唤醒第三方APP。
@Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith("alipays://") || url.startsWith("weixin://")) { try { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); } catch (Exception e) { // 处理没有安装支付宝/微信的情况 e.printStackTrace(); } return true; } return super.shouldOverrideUrlLoading(view, url); }
4. WebView内嵌的H5认证页面跳转到支付宝认证完成后,无法自动跳回APP
第三方APP在直接或间接加载契约锁认证链接,并点击跳转至支付宝APP进行认证,完成后无法自动跳回第三方APP,若需要认证后自动跳回第三方APP,需要对认证链接按照【<auth_url>&appScheme=demo://
】格式进行拼接appScheme参数(其中,"auth_url"为原始契约锁认证链接,"appScheme"为要拼接参数名,"demo://
"为需跳回App的URL Scheme):
iOS
WKWebView直接加载契约锁认证链接,直接将 "&appScheme=demo://" 拼接在原始认证链接之后:
[wkWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://auth.qiyuesuo.com?<originalQuery>&appScheme=demo://"]]];
WKWebView间接加载契约锁认证链接,需要实现WKWebView中的代理方法:
#pragma mark - <WKNavigationDelegate> - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSString *string = = navigationAction.request.URL.absoluteString; if ([string rangeOfString:@"https://auth.qiyuesuo.com"] != NSNotFound) { decisionHandler(WKNavigationActionPolicyCancel); // 截取认证链接后进行拼接 NSString *appendUrl = [string stringByAppendingString:@"&appScheme=demo://"]; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:appendUrl]]]; return; } ... // 其他第三方App业务逻辑代码 ... decisionHandler(WKNavigationActionPolicyAllow); }
Android
WebView直接加载契约锁认证链接,直接将 "&appScheme=demo://" 拼接在原始认证链接之后:
webview.loadUrl("https://auth.qiyuesuo.com?<originalQuery>&appScheme=demo://");
WebView间接加载契约锁认证链接:
webview.setWebViewClient(new WebViewClient(){ @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.contains("https://auth.qiyuesuo.com")){ // 截取认证链接后进行拼接(此处仅为参考,拼接需符合http/https请求规范) String appendUrl = url + "&appScheme=demo://"; view.loadUrl(appendUrl); return true; } return super.shouldOverrideUrlLoading(view, url); } });
5. WebView内嵌的H5认证页面认证完成后无法自动返回,或点击退出认证按钮无反应
认证的H5在认证完成后,会调用JS方法 returnApp
,APP内的WebView需要监听该JS方法的调用,以实现退出后的逻辑处理。
示例代码如下:
iOS
// 为JS方法「returnApp」添加响应对象「jsHandler」,「jsHandler」可以是实现 <WKScriptMessageHandler> 协议的任意对象 [webView.configuration.userContentController addScriptMessageHandler:jsHandler name:@"returnApp"]; //「jsHandler」实现代理方法 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if ([message.name isEqualToString:@"returnApp"]) { // 在此处编写退出页面或其他退出逻辑的代码 } }
Android
protected void addJavascriptInterface(WebView mWebView) { mWebView.addJavascriptInterface(new JsToH5Api(), "android"); } class JsToH5Api{ @JavascriptInterface public void returnApp() { // 调用finish方法退出界面 或 处理自己的退出逻辑 finish(); } }
6. WebView内嵌的H5页面无法下载文件或下载文件乱码
契约锁前端H5页面中的文件下载,并不是通过跳转资源文件目录触发下载的形式实现的,而是通过下载接口(需要登录态)使用blob触发的下载事件。所以第三方APP内嵌契约锁H5时,原生想要截获文件下载事件就必须通过监听blob,从而触发APP原生相关的下载操作。
iOS
WKWebView从iOS14.5开始新增了 WKNavigationActionPolicyDownload 的导航操作策略,可通过 WKNavigationDelegate 代理方法截获blob从而触发下载事件。
#pragma mark - <WKNavigationDelegate> - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSURL *url = navigationAction.request.URL; if ([url.absoluteString hasPrefix:@"blob"]) { // 截获blob下载事件 if (@available(iOS 14.5, *)) { decisionHandler(WKNavigationActionPolicyDownload); // 仅在iOS14.5及更新版本支持WKNavigationActionPolicyDownload } else { decisionHandler(WKNavigationActionPolicyCancel); // iOS14.5以下版本默认不支持 } return; } ... // 其他第三方App业务逻辑代码 ... decisionHandler(WKNavigationActionPolicyAllow); } // ↓↓↓以下代理方法均只适用于iOS14.5及更高版本↓↓↓ #pragma mark - <WKNavigationDelegate> - (void)webView:(WKWebView *)webView navigationAction:(WKNavigationAction *)navigationAction didBecomeDownload:(WKDownload *)download API_AVAILABLE(ios(14.5)) { download.delegate = self;// 设置WKDownloadDelegate } #pragma mark - <WKDownloadDelegate> - (void)download:(WKDownload *)download decideDestinationUsingResponse:(NSURLResponse *)response suggestedFilename:(NSString *)suggestedFilename completionHandler:(void (^)(NSURL * _Nullable))completionHandler API_AVAILABLE(ios(14.5)) { NSURL *tmpDir = [[NSFileManager defaultManager] temporaryDirectory]; // 根据具体业务场景决定下载路径 NSURL *url = [tmpDir URLByAppendingPathComponent:suggestedFilename]; // 此处仅为示例,实际场景中建议文件名去重,同样路径同样文件名会导致下载失败 completionHandler(url); // 返回目标url } - (void)downloadDidFinish:(WKDownload *)download API_AVAILABLE(ios(14.5)) { // 下载成功后的代理回调方法 } - (void)download:(WKDownload *)download didFailWithError:(NSError *)error resumeData:(nullable NSData *)resumeData API_AVAILABLE(ios(14.5)) { // 下载失败后的代理回调方法 }
Android
Android 原生不支持blob协议,但是我们可以监听前端blob下载,自己实现blob转成Base64从而进行文件下载。
// step 1 需要支持javaScript mWebview.getSettings().setJavaScriptEnabled(true); mWebview.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); // step 2 注册 javaScript监听 DownloadBlobFileJSInterface mDownloadBlobFileJSInterface = new DownloadBlobFileJSInterface(); mWebview.addJavascriptInterface(mDownloadBlobFileJSInterface, "Android"); // step 3 监听 DownloadListener,这里根据业务逻辑动态申请文件权限 mWebview.setDownloadListener(new DownloadListener() { @Override public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { if (url.startsWith("blob")) { // 动态申请文件权限 mWebview.loadUrl(DownloadBlobFileJSInterface.getBase64StringFromBlobUrl(url)); } } }); // step 4 支持监听文件下载状态 mDownloadBlobFileJSInterface.setDownloadListener(new DownloadBlobFileJSInterface.BlobDownloadListener() { @Override public void onSuccess(String absolutePath) { Log.d(getClass().getSimpleName(),String.format("文件下载成功,路径%s",absolutePath)); } @Override public void onFaild(String message) { Log.d(getClass().getSimpleName(),String.format("文件下载失败,原因%s",message)); } }); // step 5 DownloadBlobFileJSInterface 实现,仅做参考 public class DownloadBlobFileJSInterface { private BlobDownloadListener blobDownloadListener; @JavascriptInterface public void receiveBlobData(String base64Data,String fileName) { // 此处为保存文件路径,可根据实际场景自行调整 File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + File.separator + fileName); try { base64Data = base64Data.contains(",") ? base64Data.split(",")[1] : base64Data; byte[] fileBytes = Base64.decode(base64Data, 0); FileOutputStream os = new FileOutputStream(file, false); os.write(fileBytes); os.flush(); os.close(); if (blobDownloadListener != null) blobDownloadListener.onSuccess(file.getAbsolutePath()); } catch (Exception e) { e.printStackTrace(); if (blobDownloadListener != null) blobDownloadListener.onFaild(e.getMessage()); } } public static String getBase64StringFromBlobUrl(String blobUrl) { return "javascript: " + "var xhr = new XMLHttpRequest();" + "xhr.open('GET', '" + blobUrl + "', true);" + "xhr.responseType = 'blob';" + "xhr.onload = function() {" + " var blob = xhr.response;" + " var reader = new FileReader();" + " reader.onloadend = function() {" + " var base64data = reader.result.split(',')[1];" + " var contentType = blob.type;" + " var contentDisposition = xhr.getResponseHeader('Content-Disposition');" + " var fileName = 'downloaded_file';" + " if (contentDisposition) {" + " var matches = /filename=\"([^\"]+)\"/.exec(contentDisposition);" + " if (matches != null && matches[1]) { fileName = matches[1]; }" + " }else{" + " var urlParts = xhr.responseURL.split('/');" + " var possibleFileName = urlParts[urlParts.length - 1];" + " if (possibleFileName) { fileName = possibleFileName; }" + " }" + " Android.receiveBlobData(base64data, fileName);" + " };" + " reader.readAsDataURL(blob);" + "};" + "xhr.send();"; } public void setDownloadListener(BlobDownloadListener listener) { blobDownloadListener = listener; } public interface BlobDownloadListener { void onSuccess(String absolutePath); void onFaild(String message); } }
7. WebView的UserAgent相关问题
H5页面在某些场景下,需要判断当前是否处于APP的WebView内嵌环境下,此时需要第三方APP在WebView的UserAgent中,增加契约锁指定的UserAgent标识:QYSCustomerApp/private 。
iOS
获取WKWebView的完整UserAgent需要执行 navigator.userAgent,建议最好在原始的完整UserAgent末尾拼接契约锁指定标识。
[self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) { // 获取原始UserAgent NSMutableString *userAgent = [result mutableCopy]; // 判断标识是否已存在,若不存在则拼接契约锁指定的UA标识 if (![userAgent containsString:@"QYSCustomerApp/private"]) { [userAgent appendString:@" QYSCustomerApp/private"]; } // 配置全局UserAgent [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":[userAgent copy]}]; [[NSUserDefaults standardUserDefaults] synchronize]; // 配置当前WKWebView的UserAgent并且立即生效 self.webView.customUserAgent = [userAgent copy]; }];
Android
获取WebView的完整UserAgent需要执行 webView.getSettings().getUserAgentString(),建议最好在原始的完整UserAgent末尾拼接契约锁指定标识,此设置放在webview.loadUrl()之前。
String appendUa = " QYSCustomerApp/private"; WebSettings settings = webView.getSettings(); settings.setUserAgentString(settings.getUserAgentString() + appendUa);
8. Android APP无法跳转到契约锁APP
H5中存在部分操作无法在第三方APP内完成,需跳转至契约锁APP内完成相关操作;第三方APP的Android端在WebView中需要做以下处理,方可成功跳转至契约锁APP。
Android
重写 WebViewClient 的 shouldOverrideUrlLoading 方法,通过契约锁的 URL Scheme 打开契约锁APP
@Override public boolean shouldOverrideUrlLoading(WebView view, String url) { Uri uri = Uri.parse(url); if("qys".equals(uri.getScheme())){ try { Intent intent = new Intent(Intent.ACTION_VIEW, uri); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } catch (Exception e) { e.printStackTrace(); // 未安装契约锁App,跳转应用市场下载 Uri downloadApkUri = Uri.parse("market://details?id=com.genyannetwork.qiyuesuo"); Intent intent =new Intent(Intent.ACTION_VIEW, downloadApkUri); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } return true; } return super.shouldOverrideUrlLoading(view, url); }
9. Android WebView内嵌的H5页面中的图片无法长按保存至本地
Android
长按保存图片是浏览器的功能,原生WebView默认不支持,如果需要实现浏览器相似的功能,可通过监听WebView的 setOnLongClickListener 方法实现。
mWebView.setOnLongClickListener(v -> { //获取所点击的内容 final WebView.HitTestResult htr = ((WebView) v).getHitTestResult(); //判断被点击的类型为图片 if (htr.getType() == WebView.HitTestResult.IMAGE_TYPE || htr.getType() == WebView.HitTestResult.IMAGE_ANCHOR_TYPE || htr.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { String imagePath = htr.getExtra(); // TODO 下载当前图片到本地 // yourSaveImageMethod(imagePath); } return false; });
10. iOS WKWebView内嵌的H5认证页面,每一次使用活体都需要弹窗询问权限
iOS
WKWebView内嵌认证H5使用活体检测的场景下,默认会每一次都弹出获取摄像头权限的弹窗;如果不想每次认证都弹出,可以给WKWebView设置UIDelegate,并实现如下代理方法;需要注意的是,该代理方法仅适用于iOS15及更高版本。
- (void)webView:(WKWebView *)webView requestMediaCapturePermissionForOrigin:(WKSecurityOrigin *)origin initiatedByFrame:(WKFrameInfo *)frame type:(WKMediaCaptureType)type decisionHandler:(void (^)(WKPermissionDecision decision))decisionHandler API_AVAILABLE(ios(15.0)) { decisionHandler(WKPermissionDecisionGrant); // 可设置默认放行,也可根据APP自身需要调整 }
11. iOS WKWebView内嵌的H5页面部分头部内容被遮挡
iOS
iOS的WKWebView的内部容器是UIScrollView,可能存在ScrollView内边距自适应的情况,需要禁用相关设置。
if (@available(iOS 11.0, *)) { viewController.webview.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } else { viewController.automaticallyAdjustsScrollViewInsets = NO; } viewController.edgesForExtendedLayout = UIRectEdgeNone; // 视图不延伸至任何边缘