1. WebView内嵌的H5认证页面不支持实时动态检测,或实时动态检测时唤起相机黑屏、无法预览拍摄画面
iOS
WKWebView 从 iOS14.3 开始支持 getUserMedia() 的调用,但是 WKWebView.configuration 的属性 allowsInlineMediaPlayback 默认值是 NO,该属性决定了H5是否可以播放内联HTML5视频。需要将 allowsInlineMediaPlayback 属性的值置为 YES ,即可正常展示活体检测预览。注:如果使用SFSafariViewController加载H5页面,则不存在该问题。
// info.plist中添加相机权限用途描述 <key>NSCameraUsageDescription</key> <string>相机权限用途描述,文案可自行编辑</string>// 需要先初始化WKWebViewConfiguration并修改好参数,再初始化WKWebView并传入configuration,否则配置可能会不生效! WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; config.allowsInlineMediaPlayback = YES; // 默认是NO,这个值决定了用内嵌HTML5播放视频还是用本地的全屏控制 config.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; // 音视频的播放不需要用户手势触发,即为自动播放 // 初始化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
HarmonyOS NEXT
为了使腾讯云/旷世的实时动态检测能够正常生效,同时适配可能出现的实时动态检测降级到录制视频的场景,请参考如下代码进行兼容性配置,从而确保能够正常唤起相机。
首先在 model.json5 文件中增加申请ohos.permission.INTERNET、ohos.permission.CAMERA等权限,具体如下
"requestPermissions":[ { "name" : "ohos.permission.INTERNET" }, { "name" : "ohos.permission.CAMERA" }, { "name": "ohos.permission.MICROPHONE" }, { "name": "ohos.permission.WRITE_MEDIA" }, { "name": "ohos.permission.READ_MEDIA" }, { "name": "ohos.permission.MEDIA_LOCATION" } ]然后进行实时动态检测时,通过监听 requestPermissionsFromUser 动态申请权限,该方法会在即将进行实时动态检测之前触发。
let atManager = abilityAccessCtrl.createAtManager(); // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗 const permissions: Array<Permissions> = ['ohos.permission.CAMERA', 'ohos.permission.MICROPHONE', 'ohos.permission.WRITE_MEDIA','ohos.permission.READ_MEDIA', 'ohos.permission.MEDIA_LOCATION']; atManager.requestPermissionsFromUser(context,permissions).then((data) => { let grantStatus: Array<number> = data.authResults; let length: number = grantStatus.length; for (let i = 0; i < length; i++) { if (grantStatus[i] === 0) { // 用户授权,可以继续访问目标操作 } else { // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限 return; } } }) // 详细示例代码下载地址:https://dl.qiyuesuo.com/public/app/doc/webRTC-demo-harmonyos.zip
2. WebView内嵌H5认证页,音视频双录无法正常进入
iOS
WKWebView 从 iOS14.3 开始支持 getUserMedia() 的调用,但是 WKWebView.configuration 的属性 allowsInlineMediaPlayback 默认值是 NO,该属性决定了H5是否可以播放内联HTML5视频。需要将 allowsInlineMediaPlayback 属性的值置为 YES ,即可正常进入音视频双录并完成认证。注:如果使用SFSafariViewController加载H5页面,则不存在该问题。
// info.plist中添加相机和麦克风权限用途描述 <key>NSCameraUsageDescription</key> <string>相机权限用途描述,文案可自行编辑</string> <key>NSMicrophoneUsageDescription</key> <string>麦克风权限用途描述,文案可自行编辑</string>// 需要先初始化WKWebViewConfiguration并修改好参数,再初始化WKWebView并传入configuration,否则配置可能会不生效! WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; config.allowsInlineMediaPlayback = YES; // 默认是NO,这个值决定了用内嵌HTML5播放视频还是用本地的全屏控制 config.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; // 音视频的播放不需要用户手势触发,即为自动播放 // 初始化WKWebViewConfiguration后,再初始化WKWebView并传入config WKWebView *webview = [[WKWebView alloc] initWithFrame:frame configuration:config];Android
双录走 getUserMedia。原生 WebView 默认拒绝
onPermissionRequest,需先按请求资源动态申请系统权限,再grant。CAMERA/RECORD_AUDIO需动态申请;MODIFY_AUDIO_SETTINGS为普通权限、不弹框,但不声明时部分机型无声音。并设置setMediaPlaybackRequiresUserGesture(false),避免权限弹窗后预览黑屏。<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />webView.getSettings().setMediaPlaybackRequiresUserGesture(false); // 权限弹窗后手势已断,不设可能预览黑屏 private PermissionRequest pendingRequest; private static final int REQUEST_CODE_WEB_MEDIA = 0xE1; @Override public void onPermissionRequest(final PermissionRequest request) { runOnUiThread(() -> { String[] resources = request.getResources(); if (resources == null || resources.length == 0) { request.deny(); return; } List<String> missing = new ArrayList<>(); for (String resource : resources) { if (PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(resource) && ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { missing.add(Manifest.permission.CAMERA); } else if (PermissionRequest.RESOURCE_AUDIO_CAPTURE.equals(resource) && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { missing.add(Manifest.permission.RECORD_AUDIO); } } if (missing.isEmpty()) { grantMedia(request); return; } // TODO: 也可用自有权限框架替换下面两行 pendingRequest = request; ActivityCompat.requestPermissions(this, missing.toArray(new String[0]), REQUEST_CODE_WEB_MEDIA); }); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode != REQUEST_CODE_WEB_MEDIA || pendingRequest == null) { return; } grantMedia(pendingRequest); pendingRequest = null; } private void grantMedia(PermissionRequest request) { List<String> allowed = new ArrayList<>(); for (String resource : request.getResources()) { if (PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(resource) && ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { allowed.add(resource); } else if (PermissionRequest.RESOURCE_AUDIO_CAPTURE.equals(resource) && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { allowed.add(resource); } } if (allowed.isEmpty()) { request.deny(); } else { request.grant(allowed.toArray(new String[0])); } }HarmonyOS NEXT
双录走 getUserMedia。需在
module.json5声明相机、麦克风权限,并在 Web 组件的onPermissionRequest中先申请系统权限,再grant;未处理该回调时,网页侧采集会失败或一直等待。{ "name": "ohos.permission.CAMERA", "reason": "$string:camera_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { "name": "ohos.permission.MICROPHONE", "reason": "$string:microphone_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl'; import common from '@ohos.app.ability.common'; Web({ src: this.portalUrl, controller: this.webController }) .onPermissionRequest((event) => { const request = event.request const resources = request.getAccessibleResource() const permissions: Permissions[] = [] for (const resource of resources) { if (resource === ProtectedResourceType.VIDEO_CAPTURE) { permissions.push('ohos.permission.CAMERA') } else if (resource === ProtectedResourceType.AUDIO_CAPTURE) { permissions.push('ohos.permission.MICROPHONE') } } if (permissions.length === 0) { request.deny() return } const context = this.getUIContext().getHostContext() as common.UIAbilityContext abilityAccessCtrl.createAtManager() .requestPermissionsFromUser(context, permissions) .then((data) => { const granted = data.authResults.every((status) => status === 0) if (granted) { request.grant(resources) } else { request.deny() } }) .catch(() => { request.deny() }) })
3. WebView内嵌的H5页面无法正常的调用相机、相册
iOS
根据苹果商店的审核条款,需要在 info.plist 中添加请求相关权限时的提示文案,否则可能会出现闪退的情况。目前契约锁的H5中可能使用到的权限包括「相机」「相册」「麦克风」。配置项如下(已配置项无需再次配置):
// info.plist中添加相机、相册、麦克风权限用途描述 <key>NSCameraUsageDescription</key> <string>相机权限用途描述,文案可自行编辑</string> <key>NSMicrophoneUsageDescription</key> <string>麦克风权限用途描述,文案可自行编辑</string> <key>NSPhotoLibraryAddUsageDescription</key> <string>相册权限用途描述,文案可自行编辑</string> <key>NSPhotoLibraryUsageDescription</key> <string>相册权限用途描述,文案可自行编辑</string>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
4. APP内嵌契约锁认证服务H5,认证完成后无法自动返回APP原生页面
在生成认证链接时,可以把APP的URL Scheme链接作为 successPage 传入,认证结束后H5会跳转该URL Scheme,APP可通过监听WebView的路由跳转,拦截指定的URL Scheme,并做出相应处理。另外,successPage不仅限于URL Scheme链接,也可以通过传入指定的http链接并在WebView拦截来实现此功能。可参考下方代码示例,以APP URL Scheme demo:// 为例:
iOS
在 WKNavigationDelegate 的 decidePolicyForNavigationAction 中拦截指定URL Scheme,实现该代理方法前需要先设置代理对象。
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { if ([self handleAuthScheme:navigationAction.request.URL]) { decisionHandler(WKNavigationActionPolicyCancel); return; } decisionHandler(WKNavigationActionPolicyAllow); } - (BOOL)handleAuthScheme:(NSURL *)url { if (url.scheme.length == 0 || [url.scheme caseInsensitiveCompare:@"demo"] != NSOrderedSame) { return NO; } // TODO 自行处理此处业务逻辑 return YES; }Android
Android 7及以上系统回调的是带 WebResourceRequest 的方法,两个重载都要实现。
webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { if (request != null && handleAuthScheme(request.getUrl())) { return true; } return super.shouldOverrideUrlLoading(view, request); } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (handleAuthScheme(Uri.parse(url))) { return true; } return super.shouldOverrideUrlLoading(view, url); } private boolean handleAuthScheme(Uri uri) { if (uri == null || !"demo".equalsIgnoreCase(uri.getScheme())) { return false; } // TODO 自行处理此处业务逻辑 return true; } });HarmonyOS NEXT
在 onLoadIntercept 里判断scheme,返回 true 后不再继续加载。
Web({ src: this.authUrl, controller: this.controller }) .onLoadIntercept((event) => { if (this.handleAuthScheme(event.data.getRequestUrl())) { return true; } return false; }) private handleAuthScheme(requestUrl: string): boolean { if (!requestUrl || !requestUrl.toLowerCase().startsWith('demo://')) { return false; } // TODO 自行处理此处业务逻辑 return true; }
5. WebView内嵌的H5页面无法跳转到微信、支付宝等第三方APP
在没有对WebView做任何处理的情况下,无法主动唤起微信、支付宝等第三方APP。在WebView中,可以通过拦截支付宝的认证链接,以打开URL Scheme的方式唤醒第三方APP。
iOS
// info.plist中添加需要跳转的URL Scheme至白名单 <key>LSApplicationQueriesSchemes</key> <array> <string>alipays</string> <string>weixin</string> </array>实现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 [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; 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); }
6. 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); } });
7. 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(); } }
8. 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下载事件 decisionHandler(WKNavigationActionPolicyDownload); // 仅在iOS14.5及更新版本支持WKNavigationActionPolicyDownload 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); } }
9. 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);
10. 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); }
11. 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; });
12. 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自身需要调整 }
13. iOS WKWebView内嵌的H5页面部分头部内容被遮挡
iOS
iOS的WKWebView的内部容器是UIScrollView,可能存在ScrollView内边距自适应的情况,需要禁用相关设置。
viewController.webview.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; viewController.edgesForExtendedLayout = UIRectEdgeNone; // 视图不延伸至任何边缘
沪公网安备 31011202012092号