import common from '@ohos.app.ability.common';
import { TencentQianWebViewConfig } from '../model/TencentQianWebViewConfig';
import { OnSignResult, OnSdkEvent } from '../model/SignCallback';
import {
SchemeInterceptedEvent,
SchemeNotMatchedEvent,
DownloadFailedEvent
} from '../model/SdkEvent';
import { SignResultParser } from './SignResultParser';
import { ExternalSchemeHandler } from './ExternalSchemeHandler';
/** 主框架疑似下载链接的回调,容器侧据此走 DownloadHandler。 */
export type OnDirectDownload = (url: string) => void;
/**
* Scheme 拦截层(SDK 内部使用)。
*
* 对 Web 即将加载的 URL 做归类处理:
* - `qianapp://` → 解析为 SignResult,触发 `onSignResult` 回调
* - 白名单外部 scheme(weixin / alipays 等)→ startAbility 跳出本 App
* - 主框架 http(s) 链接后缀疑似下载 → 走 DownloadHandler
* - `blob:` → 暂不支持,通过 `DownloadFailed` 事件提示客户
* - 其余 http(s) / about / data / javascript → 放行 Web 组件加载
* - 未匹配的 scheme → 丢弃并通过 `SchemeNotMatched` 事件上报
*/
export class SchemeInterceptor {
/** 直连下载的文件后缀白名单(对齐 Android 下载行为)。 */
private static readonly DOWNLOAD_SUFFIXES: string[] = [
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.csv', '.ppt', '.pptx',
'.txt', '.zip', '.rar', '.7z', '.tar', '.gz',
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp',
'.mp3', '.mp4', '.m4a', '.wav', '.avi', '.mov',
'.apk', '.hap'
];
private context: common.UIAbilityContext;
private config: TencentQianWebViewConfig;
private onSignResult: OnSignResult;
private onSdkEvent?: OnSdkEvent;
private onDirectDownload?: OnDirectDownload;
constructor(
context: common.UIAbilityContext,
config: TencentQianWebViewConfig,
onSignResult: OnSignResult,
onSdkEvent?: OnSdkEvent,
onDirectDownload?: OnDirectDownload
) {
this.context = context;
this.config = config;
this.onSignResult = onSignResult;
this.onSdkEvent = onSdkEvent;
this.onDirectDownload = onDirectDownload;
}
/**
* 拦截判定。
*
* @param url 即将加载的 URL
* @returns true = SDK 已处理,Web 组件应取消导航;false = 放行
*/
public intercept(url: string): boolean {
if (url === null || url === undefined || url.length === 0) {
return false;
}
if (SignResultParser.isQianAppScheme(url)) {
return this.handleQianApp(url);
}
const scheme: string = ExternalSchemeHandler.schemeOf(url);
if (scheme === 'blob') {
this.emitBlobUnsupported(url);
return true;
}
if (scheme === 'http' || scheme === 'https') {
return this.handleHttpUrl(url);
}
if (scheme === 'about' || scheme === 'data' || scheme === 'javascript') {
return false;
}
if (ExternalSchemeHandler.isWhitelisted(url, this.config.externalSchemes)) {
this.handleExternalScheme(scheme, url);
return true;
}
this.emitSchemeNotMatched(url);
return true;
}
/** 解析 `qianapp://` 并触发 `onSignResult` 回调。 */
private handleQianApp(url: string): boolean {
this.emitEvent(new SchemeInterceptedEvent('qianapp', url));
try {
this.onSignResult(SignResultParser.parse(url));
} catch (_e) {
// callback 异常不影响 SDK
}
return true;
}
/** http(s) 链接:后缀疑似下载交给 DownloadHandler,否则放行 Web 组件。 */
private handleHttpUrl(url: string): boolean {
if (!this.isDownloadUrl(url) || this.onDirectDownload === undefined) {
return false;
}
this.emitEvent(new SchemeInterceptedEvent('download', url));
try {
this.onDirectDownload(url);
} catch (_e) {
// ignore
}
return true;
}
/** 白名单外部 scheme:`startAbility` 跳出本 App;失败上报 SchemeNotMatched。 */
private handleExternalScheme(scheme: string, url: string): void {
this.emitEvent(new SchemeInterceptedEvent(scheme, url));
ExternalSchemeHandler.open(this.context, url).then((ok: boolean) => {
if (!ok) {
this.emitSchemeNotMatched(url);
}
});
}
private emitBlobUnsupported(url: string): void {
this.emitEvent(new SchemeInterceptedEvent('blob', url));
this.emitEvent(new DownloadFailedEvent(url, 'blob 下载暂不支持,请通过 JSBridge 自行实现'));
}
private emitSchemeNotMatched(url: string): void {
this.emitEvent(new SchemeNotMatchedEvent(url));
}
private emitEvent(event: SchemeInterceptedEvent | SchemeNotMatchedEvent | DownloadFailedEvent): void {
if (this.onSdkEvent !== undefined) {
this.onSdkEvent(event);
}
}
/** 根据 URL 路径后缀判断是否疑似下载链接。 */
private isDownloadUrl(url: string): boolean {
try {
const path: string = url.split('?')[0].split('#')[0].toLowerCase();
for (const suffix of SchemeInterceptor.DOWNLOAD_SUFFIXES) {
if (path.endsWith(suffix)) {
return true;
}
}
} catch (_e) {
// ignore
}
return false;
}
}
|