1/* 2 * Copyright (c) 2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import webView from '@ohos.web.webview'; 17import router from '@ohos.router'; 18import deviceInfo from '@ohos.deviceInfo'; 19import common from '@ohos.app.ability.common'; 20import geoLocationManager from '@ohos.geoLocationManager'; 21import bundleManager from '@ohos.bundle.bundleManager'; 22import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl'; 23import connection from '@ohos.net.connection'; 24import request from '@ohos.request'; 25import fs from '@ohos.file.fs'; 26import util from '@ohos.util'; 27import photoAccessHelper from '@ohos.file.photoAccessHelper'; 28import { filePreview } from '@kit.PreviewKit'; 29import fileUri from '@ohos.file.fileuri'; 30import picker from '@ohos.multimedia.cameraPicker'; 31import filePicker from '@ohos.file.picker'; 32import { BusinessError } from '@ohos.base'; 33import { call } from '@kit.TelephonyKit'; 34import { authentication } from '@kit.AccountKit'; 35import { paymentService } from '@kit.PaymentKit'; 36import { hiAppEvent } from '@kit.PerformanceAnalysisKit'; 37 38let atomicBasicEngine: ESObject | null = null; 39 40/** 41 * 初始化加载atomicbasicengine 42 */ 43function loadAtomicBasicEngine(): void { 44 try { 45 import('@hms.atomicservicedistribution.atomicbasicengine').then((ns: ESObject) => { 46 console.log('AtomicServiceWeb loadAtomicBasicEngine success'); 47 atomicBasicEngine = ns; 48 }).catch((err: BusinessError) => { 49 console.error('AtomicServiceWeb loadAtomicBasicEngine error, message: ' + err.message); 50 }); 51 } catch (err) { 52 console.error('AtomicServiceWeb loadAtomicBasicEngine error, message: ' + err.message); 53 } 54} 55 56loadAtomicBasicEngine(); 57 58class AsError { 59 public code: number; 60 public message: string; 61 62 constructor(code: number, message: string) { 63 this.code = code; 64 this.message = message; 65 } 66} 67 68class JsApiConfig { 69 public apiName: string; 70 public minVersion: string; 71 public maxVersion: string; 72 public requiredFieldNames?: string[]; 73 74 constructor(apiName: string, minVersion: string, maxVersion: string, requiredFieldNames?: string[]) { 75 this.apiName = apiName; 76 this.minVersion = minVersion; 77 this.maxVersion = maxVersion; 78 this.requiredFieldNames = requiredFieldNames; 79 } 80} 81 82const LOG_ENABLE: boolean = true; 83const LOG_PREFIX: string = '[AtomicServiceWebLog]'; 84const UPLOAD_IMAGE_CACHE_DIR: string = '/cache/'; 85const JAVA_SCRIPT_PROXY_OBJECT_NAME: string = 'atomicServiceProxy'; 86const JAVA_SCRIPT_PROXY_API_NAME_LIST: string[] = ['invokeJsApi']; 87const ATOMIC_SERVICE_JS_API_MAP = new Map<string, JsApiConfig>(); 88const registerJsApi = (apiNameAlias: string, apiName: string, minVersion: string, maxVersion: string, 89 requiredFieldNames: string[]): void => { 90 ATOMIC_SERVICE_JS_API_MAP.set(apiNameAlias, new JsApiConfig(apiName, minVersion, maxVersion, requiredFieldNames)); 91}; 92const MAX_VERSION = '99.99.99'; 93const ATOMIC_SERVICE_JS_SDK_CURRENT_VERSION = '1.0.1'; 94const PERMISSION_APPROXIMATELY_LOCATION: Permissions = 'ohos.permission.APPROXIMATELY_LOCATION'; 95const TEL_PROTOCOL: string = 'tel:'; 96const MAILTO_PROTOCOL: string = 'mailto:'; 97const WANT_ACTION_SEND_TO_DATA: string = 'ohos.want.action.sendToData'; 98const RESOURCE_RAWFILE: string = 'resource://rawfile'; 99const TYPE_AS_WEB: string = 'ASWeb'; 100const WEB_PERMISSIONS: Record<string, string> = { 101 'TYPE_VIDEO_CAPTURE': 'ohos.permission.CAMERA', 102 'TYPE_AUDIO_CAPTURE': 'ohos.permission.MICROPHONE' 103}; 104 105const SYSTEM_INTERNAL_ERROR: AsError = new AsError(500, 'System internal error.'); 106const JS_API_INVALID_INVOKE_ERROR: AsError = new AsError(200001, 'Invalid invoke.'); 107const PARAM_REQUIRED_ERROR_CODE: number = 200002; 108const PARAM_NUMBER_POSITIVE_ERROR_CODE: number = 200003; 109const ROUTER_PARAM_MODE_INVALID_ERROR: AsError = new AsError(200004, 'Param mode is invalid.'); 110const BACK_URL_NOT_EXIST_OR_OPENED_ERROR: AsError = new AsError(200005, 'Url is not exist or opened, can not be back.'); 111const NAV_PATH_STACK_NOT_EXIST_ERROR_CODE: number = 200006; 112const POP_PATH_NAME_NOT_EXIST_ERROR: AsError = new AsError(200007, 'Name is not exist or opened, can not be pop.'); 113const POP_PATH_PARAM_INDEX_INVALID_ERROR: AsError = new AsError(200008, 'Param index is invalid.'); 114const POP_PATH_INDEX_OUT_OF_RANGE_ERROR: AsError = new AsError(200009, 'The Index is out of range.'); 115const UPLOAD_IMAGE_FILES_REQUIRED_ERROR: AsError = new AsError(200010, 'Param files is required.'); 116const UPLOAD_IMAGE_FILE_NOT_EXIST_ERROR_CODE: number = 200011; 117const UPLOAD_IMAGE_FILES_URI_REQUIRED_ERROR: AsError = new AsError(200012, 'Param uri of files is required.'); 118const UPLOAD_FILE_ERROR: AsError = new AsError(200013, 'Upload file error.'); 119const IMAGE_CAN_NOT_PREVIEW_ERROR: AsError = new AsError(200014, 'The filePath can not preview.'); 120const NETWORK_NO_ACTIVE_ERROR: AsError = new AsError(200015, 'The network is not active.'); 121const PERMISSION_LOCATION_USER_REFUSED_ERROR: number = 200016; 122const LOGIN_STATE_INVALID_ERROR: AsError = new AsError(200017, 'Login state is invalid.'); 123const LOGIN_RESPONSE_DATA_NULL_ERROR: AsError = new AsError(200018, 'Response data is null.'); 124const REQUEST_PAYMENT_ORDER_STR_INVALID_ERROR: AsError = new AsError(200019, 'orderStr is not type string.'); 125const NEED_REPORTED_API_LIST: string[] = 126 ['has.cameraPicker.pick', 'has.photoViewPicker.select', 'has.filePreview.openPreview', 'has.request.uploadFile', 'has.request.downloadFile', 127 'has.connection.getNetworkType', 'has.location.getLocation', 'has.account.login', 'has.payment.requestPayment']; 128 129registerJsApi('router.pushUrl', 'pushUrl', '1.0.0', MAX_VERSION, ['url']); 130registerJsApi('router.replaceUrl', 'replaceUrl', '1.0.0', MAX_VERSION, ['url']); 131registerJsApi('router.back', 'backUrl', '1.0.0', MAX_VERSION, []); 132registerJsApi('router.clear', 'clearUrl', '1.0.0', MAX_VERSION, []); 133registerJsApi('navPathStack.pushPath', 'pushPath', '1.0.0', MAX_VERSION, ['name']); 134registerJsApi('navPathStack.replacePath', 'replacePath', '1.0.0', MAX_VERSION, ['name']); 135registerJsApi('navPathStack.pop', 'popPath', '1.0.0', MAX_VERSION, []); 136registerJsApi('navPathStack.clear', 'clearPath', '1.0.0', MAX_VERSION, []); 137registerJsApi('asWeb.postMessage', 'postMessage', '1.0.0', MAX_VERSION, ['data']); 138registerJsApi('asWeb.getEnv', 'getEnv', '1.0.0', MAX_VERSION, []); 139registerJsApi('asWeb.checkJsApi', 'checkJsApi', '1.0.0', MAX_VERSION, ['jsApiList']); 140registerJsApi('cameraPicker.pick', 'pickCamera', '1.0.0', MAX_VERSION, ['mediaTypes', 'cameraPosition']); 141registerJsApi('photoViewPicker.select', 'selectPhoto', '1.0.0', MAX_VERSION, []); 142registerJsApi('filePreview.openPreview', 'openPreview', '1.0.0', MAX_VERSION, ['uri']); 143registerJsApi('request.uploadFile', 'uploadFile', '1.0.0', MAX_VERSION, ['url', 'files']); 144registerJsApi('request.downloadFile', 'downloadFile', '1.0.0', MAX_VERSION, ['url']); 145registerJsApi('connection.getNetworkType', 'getNetworkType', '1.0.0', MAX_VERSION, []); 146registerJsApi('location.getLocation', 'getLocation', '1.0.0', MAX_VERSION, []); 147registerJsApi('account.login', 'login', '1.0.1', MAX_VERSION, []); 148registerJsApi('payment.requestPayment', 'requestPayment', '1.0.1', MAX_VERSION, ['orderStr']); 149 150@Component 151export struct AtomicServiceWeb { 152 public src: ResourceStr | undefined = undefined; 153 public navPathStack?: NavPathStack; 154 @Prop mixedMode?: MixedMode; 155 @Prop darkMode?: WebDarkMode; 156 @Prop forceDarkAccess?: boolean; 157 @Prop nestedScroll?: NestedScrollOptions | NestedScrollOptionsExt; 158 @ObjectLink controller: AtomicServiceWebController; 159 public onMessage?: Callback<OnMessageEvent> = () => { 160 }; 161 public onErrorReceive?: Callback<OnErrorReceiveEvent> = () => { 162 }; 163 public onHttpErrorReceive?: Callback<OnHttpErrorReceiveEvent> = () => { 164 }; 165 public onPageBegin?: Callback<OnPageBeginEvent> = () => { 166 }; 167 public onPageEnd?: Callback<OnPageEndEvent> = () => { 168 }; 169 public onProgressChange?: Callback<OnProgressChangeEvent> = () => { 170 }; 171 public onControllerAttached?: VoidCallback; 172 public onLoadIntercept?: Callback<OnLoadInterceptEvent, boolean>; 173 private context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext; 174 private webViewController: webView.WebviewController = new webView.WebviewController(); 175 private schemeHandler: webView.WebSchemeHandler = new webView.WebSchemeHandler(); 176 private atomicService?: AtomicService; 177 private atomicServiceProxy?: AtomicServiceProxy; 178 179 aboutToAppear(): void { 180 if (!this.atomicService) { 181 this.atomicService = new AtomicServiceApi(this.context, this.navPathStack, this.onMessage); 182 this.atomicServiceProxy = new AtomicServiceProxy(this.atomicService); 183 } 184 185 try { 186 let bundleInfo: bundleManager.BundleInfo = bundleManager.getBundleInfoForSelfSync( 187 bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION); 188 if (bundleInfo?.appInfo?.appProvisionType === 'debug') { 189 console.log(`AtomicServiceWeb setWebDebuggingAccess`); 190 webView.WebviewController.setWebDebuggingAccess(true); 191 } 192 } catch (err) { 193 console.error(`AtomicServiceWeb set Web Debug Mode failed, code is ${err.code}, message is ${err.message}`); 194 } 195 196 this.initDomainCheckLog(); 197 HiAnalyticsUtil.reportComponentEvent(); 198 } 199 200 aboutToDisappear(): void { 201 this.atomicService?.notifyMessage(); 202 } 203 204 build() { 205 Web({ src: this.src, controller: this.webViewController }) 206 .zoomAccess(false) 207 .allowWindowOpenMethod(false) 208 .domStorageAccess(true) 209 .layoutMode(WebLayoutMode.NONE) 210 .mixedMode(this.mixedMode) 211 .darkMode(this.darkMode) 212 .forceDarkAccess(this.forceDarkAccess) 213 .nestedScroll(this.nestedScroll) 214 .onErrorReceive((event: OnErrorReceiveEvent) => this.onCommonCallBack('onErrorReceive', event, 215 this.onErrorReceive)) 216 .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.onCommonCallBack('onHttpErrorReceive', event, 217 this.onHttpErrorReceive)) 218 .onPageBegin((event: OnPageBeginEvent) => this.onCommonCallBack('onPageBegin', event, this.onPageBegin)) 219 .onPageEnd((event: OnPageEndEvent) => this.onCommonCallBack('onPageEnd', event, this.onPageEnd)) 220 .onProgressChange((event: OnProgressChangeEvent) => this.onCommonCallBack('onProgressChange', event, 221 this.onProgressChange)) 222 .onControllerAttached(() => { 223 this.registerJavaScriptProxy(); 224 this.schemeHandler.onRequestStart((request: webView.WebSchemeHandlerRequest) => { 225 return !this.interceptUrl(request.getRequestUrl(), request.isMainFrame(), request.getRequestResourceType()); 226 }); 227 this.webViewController.setWebSchemeHandler('http', this.schemeHandler); 228 this.webViewController.setWebSchemeHandler('https', this.schemeHandler); 229 this.initAtomicServiceWebController(); 230 if (this.onControllerAttached) { 231 try { 232 this.onControllerAttached(); 233 } catch (error) { 234 console.error(`AtomicServiceWeb onControllerAttached failed, code is ${error.code}, message is ${error.message}`); 235 } 236 } 237 }) 238 .onOverrideUrlLoading((webResourceRequest: WebResourceRequest) => { 239 return !this.interceptOverrideUrlLoading(webResourceRequest.getRequestUrl()); 240 }) 241 .onLoadIntercept(event => { 242 let checkResult = !this.checkUrl(event.data.getRequestUrl()); 243 if (!checkResult && this.onLoadIntercept) { 244 try { 245 return this.onLoadIntercept(event); 246 } catch (error) { 247 console.error(`AtomicServiceWeb onLoadIntercept failed, code is ${error.code}, message is ${error.message}`); 248 return true; 249 } 250 } 251 return checkResult; 252 }) 253 .onPermissionRequest((event: OnPermissionRequestEvent) => { 254 this.handleOnPermissionRequest(event); 255 }) 256 } 257 258 onCommonCallBack<T>(method: string, event: T, callback?: (event: T) => void): void { 259 try { 260 callback && callback(event); 261 } catch (error) { 262 console.error(`AtomicServiceWeb ${method} failed, code is ${error.code}, message is ${error.message}`); 263 } 264 } 265 266 registerJavaScriptProxy(): void { 267 try { 268 this.webViewController.registerJavaScriptProxy(this.atomicServiceProxy, JAVA_SCRIPT_PROXY_OBJECT_NAME, 269 JAVA_SCRIPT_PROXY_API_NAME_LIST); 270 } catch (error) { 271 let e: BusinessError = error as BusinessError; 272 console.error(`AtomicServiceWeb registerJavaScriptProxy failed, code is ${e.code}, message is ${e.message}`); 273 } 274 } 275 276 initAtomicServiceWebController(): void { 277 if (!this.controller) { 278 return; 279 } 280 this.controller.setWebviewController(this.webViewController); 281 } 282 283 cutUrl(url: string): string { 284 if (url) { 285 let index: number = url.indexOf('?'); 286 if (index > -1) { 287 return url.substring(0, index); 288 } 289 } 290 return url; 291 } 292 293 checkUrl(url: string): boolean { 294 if (!url) { 295 return false; 296 } 297 if (url.startsWith('resource://rawfile')) { 298 return true; 299 } 300 url = this.cutUrl(url); 301 return true; 302 } 303 304 /** 305 * 初始化域名校验日志 306 */ 307 initDomainCheckLog(): void { 308 try { 309 let appId: string | null = this.getAppId(); 310 let checkDomainBlockListAvailable: boolean = this.isCheckDomainBlockListAvailable(); 311 console.debug('AtomicServiceWeb initDomainCheckLog appId=' + appId + ' checkDomainBlockListAvailable=' + 312 checkDomainBlockListAvailable); 313 } catch (err) { 314 console.error('AtomicServiceWeb initDomainCheckLog error, message: ' + err.message); 315 } 316 } 317 318 /** 319 * 获取appid 320 */ 321 getAppId(): string | null { 322 let bundleName: string = this.context.abilityInfo.bundleName; 323 if (!bundleName) { 324 return null; 325 } 326 let strArray: string[] = bundleName.split('.'); 327 if (!strArray || strArray.length <= 0) { 328 return null; 329 } 330 return strArray[strArray.length - 1]; 331 } 332 333 /** 334 * 是否主页面或iframe页面请求 335 */ 336 isMainPageOrIframeRequest(isMainFrame: boolean, requestResourceType: number): boolean { 337 if (isMainFrame) { 338 return true; 339 } 340 if (requestResourceType === webView.WebResourceType.MAIN_FRAME || 341 requestResourceType === webView.WebResourceType.SUB_FRAME) { 342 return true; 343 } 344 return false; 345 } 346 347 /** 348 * 检查checkDomainBlockList接口是否可用 349 */ 350 isCheckDomainBlockListAvailable(): boolean { 351 if (!atomicBasicEngine || !atomicBasicEngine.default) { 352 return false; 353 } 354 return typeof atomicBasicEngine.default.checkDomainBlockList === 'function'; 355 } 356 357 /** 358 * 拦截url请求 359 */ 360 interceptUrl(url: string, isMainFrame: boolean, requestResourceType: number): boolean { 361 if (!url) { 362 return false; 363 } 364 if (url.startsWith(RESOURCE_RAWFILE)) { 365 return true; 366 } 367 // 主页面或iframe页面,走域名白名单校验 368 if (this.isMainPageOrIframeRequest(isMainFrame, requestResourceType)) { 369 return this.checkUrl(url); 370 } 371 // 非主页面或iframe页面,atomicBasicEngine可用,走域名黑名单校验 372 if (this.isCheckDomainBlockListAvailable()) { 373 return this.checkUrlNew(url); 374 } 375 // 非主页面或iframe页面,atomicBasicEngine不可用,放通管控 376 return true; 377 } 378 379 /** 380 * 域名黑名单校验 381 */ 382 checkUrlNew(url: string): boolean { 383 let appId: string | null = this.getAppId(); 384 if (!appId) { 385 console.error('AtomicServiceWeb checkUrlNew error, appId is invalid'); 386 return false; 387 } 388 try { 389 let shortUrl: string = this.cutUrl(url); 390 let isInDomainBlockList: boolean = atomicBasicEngine.default.checkDomainBlockList(shortUrl, appId); 391 console.debug(`AtomicServiceWeb checkUrlNew ret=${!isInDomainBlockList} url=${shortUrl}`); 392 return !isInDomainBlockList; 393 } catch (err) { 394 console.error(`AtomicServiceWeb checkUrlNew error, code: ${err.code}, message: ${err.message}`); 395 // 黑名单接口调用失败,放通管控 396 return true; 397 } 398 } 399 400 /** 401 * 拦截url跳转 402 */ 403 interceptOverrideUrlLoading(url: string): boolean { 404 if (!url) { 405 return false; 406 } 407 // 处理tel:协议 408 if (url.startsWith(TEL_PROTOCOL)) { 409 this.openMakeCall(url); 410 return false; 411 } 412 // 处理mailto:协议 413 if (url.startsWith(MAILTO_PROTOCOL)) { 414 this.openSendMail(url); 415 return false; 416 } 417 return this.checkUrl(url); 418 } 419 420 /** 421 * 拉起打电话 422 */ 423 openMakeCall(url: string): void { 424 if (!url || !url.startsWith(TEL_PROTOCOL)) { 425 return; 426 } 427 try { 428 let phoneNumber: string = url.substring(TEL_PROTOCOL.length); 429 call.makeCall(phoneNumber).catch((err: BusinessError) => { 430 console.error(`AtomicServiceWeb openMakeCall error, code: ${err.code}, message: ${err.message}`); 431 }); 432 } catch (err) { 433 console.error(`AtomicServiceWeb openMakeCall error, code: ${err.code}, message: ${err.message}`); 434 } 435 } 436 437 /** 438 * 拉起发邮件 439 */ 440 openSendMail(url: string): void { 441 if (!url || !url.startsWith(MAILTO_PROTOCOL)) { 442 return; 443 } 444 try { 445 this.context.startAbility({ 446 action: WANT_ACTION_SEND_TO_DATA, 447 uri: url 448 }).catch((err: BusinessError) => { 449 console.error(`AtomicServiceWeb openSendMail error, code: ${err.code}, message: ${err.message}`); 450 }); 451 } catch (err) { 452 console.error(`AtomicServiceWeb openSendMail error, code: ${err.code}, message: ${err.message}`); 453 } 454 } 455 456 /** 457 * 处理onPermissionRequest回调 458 */ 459 private handleOnPermissionRequest(event: OnPermissionRequestEvent): void { 460 if (this.checkPermissionRequest(event)) { 461 event.request.grant(event.request.getAccessibleResource()); 462 } else { 463 event.request.deny(); 464 } 465 } 466 467 /** 468 * onPermissionRequest权限校验 469 */ 470 private checkPermissionRequest(event: OnPermissionRequestEvent): boolean { 471 let accessibleResource: string[] = event.request.getAccessibleResource(); 472 if (!accessibleResource || accessibleResource.length <= 0) { 473 return false; 474 } 475 let appId: string | null = this.getAppId(); 476 if (!appId) { 477 console.error('AtomicServiceWeb checkPermissionRequest error, appId is invalid'); 478 return false; 479 } 480 for (let resource of accessibleResource) { 481 let permission: string = WEB_PERMISSIONS[resource]; 482 if (!permission) { 483 console.error('AtomicServiceWeb checkPermissionRequest error, permission is not support'); 484 return false; 485 } 486 if (!this.isPermissionUserGranted(permission)) { 487 return false; 488 } 489 if (!this.isPermissionWhiteListAllow(appId, permission)) { 490 return false; 491 } 492 } 493 return true; 494 } 495 496 /** 497 * 检查用户是否授予权限 498 */ 499 private isPermissionUserGranted(permission: string): boolean { 500 try { 501 let bundleInfo: bundleManager.BundleInfo = 502 bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION); 503 if (!bundleInfo?.appInfo?.accessTokenId) { 504 return false; 505 } 506 let tokenId: number = bundleInfo.appInfo.accessTokenId; 507 let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.createAtManager() 508 .checkAccessTokenSync(tokenId, permission as Permissions); 509 if (grantStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) { 510 console.error(`AtomicServiceWeb isPermissionGranted permission ${permission} is not grant`); 511 return false; 512 } 513 return true; 514 } catch (err) { 515 console.error(`AtomicServiceWeb isPermissionGranted error, code: ${err.code}, message: ${err.message}`); 516 return false; 517 } 518 } 519 520 /** 521 * 检查权限白名单是否通过 522 */ 523 private isPermissionWhiteListAllow(appId: string, permission: string): boolean { 524 if (!atomicBasicEngine || !atomicBasicEngine.default || 525 typeof atomicBasicEngine.default.checkAtomicServiceAllow !== 'function') { 526 console.error('AtomicServiceWeb isPermissionRequestAllow error, checkAtomicServiceAllow is not available'); 527 return false; 528 } 529 try { 530 let isAllow: boolean = atomicBasicEngine.default.checkAtomicServiceAllow(appId, permission, TYPE_AS_WEB); 531 console.debug(`AtomicServiceWeb isPermissionRequestAllow ret=${isAllow} permission=${permission}`); 532 return isAllow; 533 } catch (err) { 534 console.error(`AtomicServiceWeb isPermissionRequestAllow error, code: ${err.code}, message: ${err.message}`); 535 return false; 536 } 537 } 538} 539 540@Observed 541export class AtomicServiceWebController { 542 private webViewController?: webView.WebviewController; 543 544 setWebviewController(webViewController: webView.WebviewController): void { 545 this.webViewController = webViewController; 546 } 547 548 checkWebviewController(): void { 549 if (!this.webViewController) { 550 const error: BusinessError<string> = { 551 name: '', 552 message: 'Init error. The AtomicServiceWebController must be associated with a AtomicServiceWeb component.', 553 code: 17100001, 554 } 555 throw error as Error; 556 } 557 } 558 559 getUserAgent(): string | undefined { 560 this.checkWebviewController(); 561 return this.webViewController?.getUserAgent(); 562 } 563 564 getCustomUserAgent(): string | undefined { 565 this.checkWebviewController(); 566 return this.webViewController?.getCustomUserAgent(); 567 } 568 569 setCustomUserAgent(userAgent: string): void { 570 this.checkWebviewController(); 571 this.webViewController?.setCustomUserAgent(userAgent); 572 } 573 574 accessForward(): boolean | undefined { 575 this.checkWebviewController(); 576 return this.webViewController?.accessForward(); 577 } 578 579 accessBackward(): boolean | undefined { 580 this.checkWebviewController(); 581 return this.webViewController?.accessBackward(); 582 } 583 584 accessStep(step: number): boolean | undefined { 585 this.checkWebviewController(); 586 return this.webViewController?.accessStep(step); 587 } 588 589 forward(): void { 590 this.checkWebviewController(); 591 this.webViewController?.forward(); 592 } 593 594 backward(): void { 595 this.checkWebviewController(); 596 this.webViewController?.backward(); 597 } 598 599 refresh(): void { 600 this.checkWebviewController(); 601 this.webViewController?.refresh(); 602 } 603 604 loadUrl(url: string | Resource, headers?: Array<WebHeader>): void { 605 this.checkWebviewController(); 606 if (headers) { 607 this.webViewController?.loadUrl(url, headers); 608 } else { 609 this.webViewController?.loadUrl(url); 610 } 611 } 612} 613 614class AtomicServiceProxy { 615 private atomicService: AtomicService; 616 617 constructor(atomicService: AtomicService) { 618 this.atomicService = atomicService; 619 } 620 621 invokeJsApi<T>(apiNameAlias: string, options: BaseOptions<T>): void { 622 try { 623 this.atomicService.reportBeginEvent(apiNameAlias); 624 options = options || {}; 625 if (options instanceof Object) { 626 options.apiNameAlias = apiNameAlias; 627 } 628 if (!apiNameAlias || !ATOMIC_SERVICE_JS_API_MAP.has(apiNameAlias)) { 629 this.atomicService.errorWithCodeAndMsg(JS_API_INVALID_INVOKE_ERROR, options); 630 return; 631 } 632 let jsApiConfig: JsApiConfig | undefined = ATOMIC_SERVICE_JS_API_MAP.get(apiNameAlias); 633 if (!this.atomicService.checkRequiredFieldInOptions(jsApiConfig, options)) { 634 return; 635 } 636 let atomicService: object = this.atomicService; 637 atomicService[jsApiConfig?.apiName as string](options); 638 } catch (err) { 639 this.atomicService.error(err, options); 640 } 641 } 642} 643 644class AtomicService { 645 protected context: common.UIAbilityContext; 646 protected navPathStack?: NavPathStack; 647 protected messageDataList: object[] = []; 648 protected onMessage: (event: OnMessageEvent) => void = () => { 649 }; 650 651 constructor(context: common.UIAbilityContext, navPathStack?: NavPathStack, 652 onMessage?: (event: OnMessageEvent) => void) { 653 this.context = context; 654 this.navPathStack = navPathStack; 655 this.onMessage = onMessage ? onMessage : this.onMessage; 656 } 657 658 success<T>(res: T, options: BaseOptions<T>): void { 659 this.reportSuccessEvent(options.apiNameAlias); 660 try { 661 options?.callback && options?.callback(undefined, res); 662 } catch (err) { 663 this.consoleError(`callback error, code is ${err.code}, message is ${err.message}`); 664 } 665 } 666 667 error<T>(err: BusinessError, options: BaseOptions<T>,): void { 668 this.reportFailEvent(options.apiNameAlias, err.code); 669 try { 670 options?.callback && options?.callback(new AsError(err.code ? err.code : SYSTEM_INTERNAL_ERROR.code, 671 err.message ? err.message : SYSTEM_INTERNAL_ERROR.message)); 672 } catch (err) { 673 this.consoleError(`callback error, code is ${err.code}, message is ${err.message}`); 674 } 675 } 676 677 errorWithCodeAndMsg<T>(error: AsError, options: BaseOptions<T>): void { 678 this.reportFailEvent(options.apiNameAlias, error.code); 679 try { 680 options?.callback && options?.callback(error); 681 } catch (err) { 682 this.consoleError(`callback error, code is ${err.code}, message is ${err.message}`); 683 } 684 } 685 686 consoleLog(msg: string): void { 687 if (LOG_ENABLE) { 688 console.log(`${LOG_PREFIX} ${msg}`); 689 } 690 } 691 692 consoleError(msg: string): void { 693 if (LOG_ENABLE) { 694 console.error(`${LOG_PREFIX} ${msg}`); 695 } 696 } 697 698 logOptions<T>(name: string, options: BaseOptions<T>): void { 699 this.consoleLog(`${name} options=${JSON.stringify(options)}`); 700 } 701 702 checkParamRequired<V, T>(paramKey: string, paramValue: V, options: BaseOptions<T>): boolean { 703 if (paramValue === undefined || paramValue === null || paramValue === '') { 704 this.errorWithCodeAndMsg(new AsError(PARAM_REQUIRED_ERROR_CODE, `Param ${paramKey} is required.`), options); 705 return false; 706 } 707 return true; 708 } 709 710 checkNumberParamPositive<T>(paramKey: string, paramValue: number, options: BaseOptions<T>): boolean { 711 if (paramValue <= 0) { 712 this.errorWithCodeAndMsg(new AsError(PARAM_NUMBER_POSITIVE_ERROR_CODE, 713 `Param ${paramKey} must be a positive number.`), options); 714 return false; 715 } 716 return true; 717 } 718 719 checkRequiredFieldInOptions<T>(jsApiConfig: JsApiConfig | undefined, options: BaseOptions<T>): boolean { 720 if (!jsApiConfig) { 721 return false; 722 } 723 if (!jsApiConfig.requiredFieldNames) { 724 return true; 725 } 726 let obj: object = options; 727 for (let i = 0; i < jsApiConfig.requiredFieldNames.length; i++) { 728 let fieldName: string = jsApiConfig.requiredFieldNames[i]; 729 if (!this.checkParamRequired(fieldName, obj[fieldName], options)) { 730 return false; 731 } 732 } 733 return true; 734 } 735 736 checkRouterMode<T>(mode: string | undefined, options: BaseOptions<T>): boolean { 737 if (!mode || mode === 'Single' || mode === 'Standard') { 738 return true; 739 } 740 this.errorWithCodeAndMsg(ROUTER_PARAM_MODE_INVALID_ERROR, options); 741 return false; 742 } 743 744 parseRouterMode(routerMode?: string): router.RouterMode { 745 return routerMode === 'Single' ? router.RouterMode.Single : router.RouterMode.Standard; 746 } 747 748 getRouterIndexByDelta(delta: number): number { 749 let length: number = Number.parseInt(router.getLength()); 750 for (let i = length; i > 0; i--) { 751 let state = router.getStateByIndex(i); 752 if (state?.name && delta-- == 0) { 753 return i; 754 } 755 } 756 return 1; 757 } 758 759 checkBackUrlExists<T>(url: string, options: BaseOptions<T>): boolean { 760 let length: number = Number.parseInt(router.getLength()); 761 for (let i = length; i > 0; i--) { 762 let state = router.getStateByIndex(i); 763 if (state?.name) { 764 let stateUrl: string = state?.path + state?.name; 765 if (stateUrl === url) { 766 return true; 767 } 768 } 769 } 770 this.errorWithCodeAndMsg(BACK_URL_NOT_EXIST_OR_OPENED_ERROR, options); 771 return false; 772 } 773 774 checkNavPathStack<T>(apiName: string, options: BaseOptions<T>): boolean { 775 if (!this.navPathStack) { 776 this.errorWithCodeAndMsg(new AsError(NAV_PATH_STACK_NOT_EXIST_ERROR_CODE, 777 `Current page is not NavDestination, not support ${apiName}().`), options); 778 return false; 779 } 780 return true; 781 } 782 783 getNavPathIndexByDelta(delta: number): number { 784 let pathStack: string[] | undefined = this.navPathStack?.getAllPathName(); 785 if (!pathStack || pathStack.length == 0) { 786 return -1; 787 } 788 return pathStack.length > delta ? (pathStack.length - delta - 1) : -1; 789 } 790 791 onPopHandler(popInfo: PopInfo, onPop?: (event: OnPopEvent) => void): void { 792 if (!popInfo?.info || !onPop) { 793 return; 794 } 795 onPop(new OnPopEvent(popInfo.info.name, popInfo.info.param as object, popInfo.result)); 796 } 797 798 getCurrentNavPathInfo(): NavPathInfo { 799 let navPathStack: Array<string> | undefined = this.navPathStack?.getAllPathName(); 800 let navPathInfo: NavPathInfo = (navPathStack && navPathStack.length > 0) ? 801 new NavPathInfo(navPathStack[navPathStack.length - 1], navPathStack.length - 1) : new NavPathInfo(undefined, -1); 802 if (navPathInfo.index >= 0) { 803 navPathInfo.param = this.navPathStack?.getParamByIndex(navPathInfo.index) as object; 804 } 805 return navPathInfo; 806 } 807 808 notifyMessage(): void { 809 if (this.messageDataList.length <= 0) { 810 return; 811 } 812 try { 813 this.onMessage(new OnMessageEvent(this.messageDataList)); 814 } catch (err) { 815 this.consoleError(`onMessage failed, code is ${err.code}, message is ${err.message}`); 816 } 817 this.messageDataList = []; 818 } 819 820 isJsApiEnable(jsApiConfig?: JsApiConfig): boolean { 821 if (!jsApiConfig) { 822 return false; 823 } 824 if (this.compareVersion(jsApiConfig.minVersion, ATOMIC_SERVICE_JS_SDK_CURRENT_VERSION) && 825 this.compareVersion(ATOMIC_SERVICE_JS_SDK_CURRENT_VERSION, jsApiConfig.maxVersion)) { 826 return true; 827 } 828 return false; 829 } 830 831 compareVersion(lowVersion: string, highVersion: string): boolean { 832 if (!lowVersion || !highVersion) { 833 return false; 834 } 835 let v1 = lowVersion.split('.').map(m => Number.parseInt(m)); 836 let v2 = highVersion.split('.').map(m => Number.parseInt(m)); 837 const maxLength = Math.max(v1.length, v2.length); 838 for (let i = 0; i < maxLength; i++) { 839 if (v1[i] < v2[i]) { 840 return true; 841 } else if (v1[i] > v2[i]) { 842 return false; 843 } 844 } 845 if (v1.length < v2.length) { 846 return true; 847 } 848 if (v1.length > v2.length) { 849 return false; 850 } 851 return true; 852 } 853 854 getUri(uriOrFilePath: string): string { 855 if (!uriOrFilePath || uriOrFilePath.startsWith('file://')) { 856 return uriOrFilePath; 857 } 858 return fileUri.getUriFromPath(uriOrFilePath); 859 } 860 861 async checkUploadFile(options: UploadFileOptions): Promise<CheckUploadFileResult> { 862 if (!options.files || options.files.length <= 0) { 863 this.errorWithCodeAndMsg(UPLOAD_IMAGE_FILES_REQUIRED_ERROR, options); 864 return new CheckUploadFileResult(false); 865 } 866 let uriMap: Map<string, string> = new Map(); 867 for (let i = 0; i < options.files?.length; i++) { 868 let file: UploadFile = options.files[i]; 869 if (!file.uri) { 870 this.errorWithCodeAndMsg(UPLOAD_IMAGE_FILES_URI_REQUIRED_ERROR, options); 871 return new CheckUploadFileResult(false); 872 } 873 if (!file.uri.startsWith('file://') && !fs.accessSync(file.uri, fs.AccessModeType.EXIST)) { 874 this.errorWithCodeAndMsg(new AsError(UPLOAD_IMAGE_FILE_NOT_EXIST_ERROR_CODE, 875 `File uri ${file.uri} is not exist.`), options); 876 return new CheckUploadFileResult(false); 877 } 878 let originUri: string = file.uri; 879 let uploadUri: string = file.uri; 880 if (uploadUri.indexOf(UPLOAD_IMAGE_CACHE_DIR) < 0) { 881 let srcUri: string = uploadUri.startsWith('file://') ? uploadUri : fileUri.getUriFromPath(file.uri); 882 uploadUri = this.context.cacheDir + '/' + uploadUri.substring(uploadUri.lastIndexOf('/') + 1); 883 try { 884 await fs.copy(srcUri, fileUri.getUriFromPath(uploadUri)) 885 } catch (err) { 886 this.errorWithCodeAndMsg(UPLOAD_FILE_ERROR, options); 887 return new CheckUploadFileResult(false); 888 } 889 } 890 file.uri = 'internal://' + uploadUri.substring(uploadUri.indexOf(UPLOAD_IMAGE_CACHE_DIR) + 1); 891 uriMap.set(uploadUri, originUri); 892 } 893 return new CheckUploadFileResult(true, uriMap); 894 } 895 896 convertToRequestData(data?: UploadRequestData[]): request.RequestData[] { 897 let requestData: request.RequestData[] = []; 898 if (data) { 899 data.forEach(item => { 900 if (!item.name || !item.value) { 901 return; 902 } 903 requestData.push({ name: item.name, value: item.value }); 904 }); 905 } 906 return requestData; 907 } 908 909 convertToFile(files?: UploadFile[]): request.File[] { 910 let requestFiles: request.File[] = []; 911 if (files) { 912 files.forEach(item => { 913 requestFiles.push({ 914 filename: item.filename, 915 name: item.name, 916 uri: item.uri, 917 type: item.type 918 }); 919 }); 920 } 921 return requestFiles; 922 } 923 924 handleUploadFileResult(taskStateArray: Array<request.TaskState>, uriMap: Map<string, string>, 925 options: UploadFileOptions): void { 926 let taskStates: UploadFileTaskState[] = []; 927 if (taskStateArray) { 928 taskStateArray.forEach(taskState => { 929 let path: (string | undefined) = taskState.path ? uriMap.get(taskState.path) : taskState.path; 930 taskStates.push(new UploadFileTaskState(path ? path : taskState.path, taskState.responseCode, 931 taskState.message)); 932 }); 933 } 934 this.success(new UploadFileResult(taskStates), options); 935 } 936 937 parseFileNameFromUrl(url?: string): string { 938 if (!url) { 939 return ''; 940 } 941 let http: string = url.split('?')[0]; 942 if (http.indexOf('/') < 0) { 943 return ''; 944 } 945 let index: number = http.lastIndexOf('/'); 946 if (index == (http.length - 1)) { 947 return ''; 948 } 949 return http.substring(index + 1); 950 } 951 952 checkAccessToken(permissionName: Permissions): Promise<abilityAccessCtrl.GrantStatus> { 953 let bundleInfo: bundleManager.BundleInfo = bundleManager.getBundleInfoForSelfSync( 954 bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION); 955 let tokenId: number = bundleInfo.appInfo.accessTokenId; 956 let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); 957 return atManager.checkAccessToken(tokenId, permissionName); 958 } 959 960 checkPermissions(permissionName: Permissions, grantCallback: (err?: BusinessError) => void): void { 961 this.checkAccessToken(permissionName).then(grantStatus => { 962 if (grantStatus == abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) { 963 grantCallback(undefined); 964 } else { 965 let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); 966 atManager.requestPermissionsFromUser(this.context, [permissionName]).then(permissionRequestResult => { 967 for (let i = 0; i < permissionRequestResult.authResults.length; i++) { 968 if (permissionRequestResult.authResults[i] != 0) { 969 const error: BusinessError<void> = { 970 name: '', 971 message: `RequestPermissionsFromUser error. authResult: ${permissionRequestResult.authResults[i]}.`, 972 code: PERMISSION_LOCATION_USER_REFUSED_ERROR 973 }; 974 grantCallback(error); 975 return; 976 } 977 } 978 grantCallback(undefined); 979 }).catch((err: BusinessError) => { 980 grantCallback(err); 981 }); 982 } 983 }).catch((err: BusinessError) => { 984 grantCallback(err); 985 }); 986 } 987 988 /** 989 * 判断是否需要打点 990 * 991 * @param apiNameAlias 接口别名 992 * @return 是否需要打点 993 */ 994 public isNeedReport(apiName: string): boolean { 995 return NEED_REPORTED_API_LIST.includes(apiName); 996 } 997 998 /** 999 * 上报Api调用开始的打点 1000 * 1001 * @param apiNameAlias 接口别名 1002 */ 1003 public reportBeginEvent(apiNameAlias?: string): void { 1004 const apiName: string = `has.${apiNameAlias}`; 1005 if (this.isNeedReport(apiName)) { 1006 HiAnalyticsUtil.reportApiEvent(apiName, ActionState.BEGIN); 1007 } 1008 } 1009 1010 /** 1011 * 上报Api调用成功的打点 1012 * 1013 * @param apiNameAlias 接口别名 1014 */ 1015 public reportSuccessEvent(apiNameAlias?: string): void { 1016 const apiName: string = `has.${apiNameAlias}`; 1017 if (this.isNeedReport(apiName)) { 1018 HiAnalyticsUtil.reportApiEvent(apiName, ActionState.SUCCESS); 1019 } 1020 } 1021 1022 /** 1023 * 上报Api调用失败的打点 1024 * 1025 * @param apiNameAlias 接口别名 1026 * @param errCode 错误码 1027 */ 1028 public reportFailEvent(apiNameAlias?: string, errCode?: number): void { 1029 const apiName: string = `has.${apiNameAlias}`; 1030 if (this.isNeedReport(apiName)) { 1031 HiAnalyticsUtil.reportApiEvent(apiName, ActionState.FAIL, errCode); 1032 } 1033 } 1034} 1035 1036class AtomicServiceApi extends AtomicService { 1037 constructor(context: common.UIAbilityContext, navPathStack?: NavPathStack, 1038 onMessage?: (event: OnMessageEvent) => void) { 1039 super(context, navPathStack, onMessage); 1040 } 1041 1042 pushUrl(options: PushUrlOptions): void { 1043 if (!this.checkRouterMode(options.mode, options)) { 1044 return; 1045 } 1046 router.pushUrl({ url: options.url, params: options.params }, this.parseRouterMode(options.mode)).then(() => { 1047 this.success(new PushUrlResult(), options); 1048 }).catch((err: BusinessError) => { 1049 this.error(err, options); 1050 }); 1051 } 1052 1053 replaceUrl(options: ReplaceUrlOptions): void { 1054 if (!this.checkRouterMode(options.mode, options)) { 1055 return; 1056 } 1057 router.replaceUrl({ url: options.url, params: options.params }, this.parseRouterMode(options.mode)).then(() => { 1058 this.success(new ReplaceUrlResult(), options); 1059 }).catch((err: BusinessError) => { 1060 this.error(err, options); 1061 }); 1062 } 1063 1064 backUrl(options: BackUrlOptions): void { 1065 if (options.url) { 1066 if (!this.checkBackUrlExists(options.url, options)) { 1067 return; 1068 } 1069 router.back({ url: options.url, params: options.params }); 1070 this.success(new BackUrlResult(), options); 1071 } else if (options.index || options.index === 0) { 1072 if (!this.checkNumberParamPositive('index', options.index, options)) { 1073 return; 1074 } 1075 router.back(options.index, options.params); 1076 this.success(new BackUrlResult(), options); 1077 } else if (options.delta || options.delta === 0) { 1078 if (!this.checkNumberParamPositive('delta', options.delta, options)) { 1079 return; 1080 } 1081 router.back(this.getRouterIndexByDelta(options.delta), options.params); 1082 this.success(new BackUrlResult(), options); 1083 } else { 1084 router.back(); 1085 this.success(new BackUrlResult(), options); 1086 } 1087 } 1088 1089 clearUrl(options: ClearUrlOptions): void { 1090 router.clear(); 1091 this.success(new ClearUrlResult(), options); 1092 } 1093 1094 pushPath(options: PushPathOptions): void { 1095 if (!this.checkNavPathStack('navPathStack.pushPath', options)) { 1096 return; 1097 } 1098 this.navPathStack?.pushPath({ 1099 name: options.name, 1100 param: options.param, 1101 onPop: popInfo => this.onPopHandler(popInfo, options.onPop) 1102 }, options.animated); 1103 this.success(new PushPathResult(), options); 1104 } 1105 1106 replacePath(options: ReplacePathOptions): void { 1107 if (!this.checkNavPathStack('navPathStack.replacePath', options)) { 1108 return; 1109 } 1110 this.navPathStack?.replacePath({ 1111 name: options.name, 1112 param: options.param, 1113 onPop: popInfo => this.onPopHandler(popInfo, options.onPop) 1114 }, options.animated); 1115 this.success(new ReplacePathResult(), options); 1116 } 1117 1118 popPath(options: PopPathOptions): void { 1119 if (!this.checkNavPathStack('navPathStack.pop', options)) { 1120 return; 1121 } 1122 if (options.name) { 1123 let index: number | undefined = this.navPathStack?.popToName(options.name, options.result, options.animated); 1124 if (index === undefined || index === -1) { 1125 this.errorWithCodeAndMsg(POP_PATH_NAME_NOT_EXIST_ERROR, options); 1126 return; 1127 } 1128 } else if (options.index || options.index === 0) { 1129 if (options.index < -1) { 1130 this.errorWithCodeAndMsg(POP_PATH_PARAM_INDEX_INVALID_ERROR, options); 1131 return; 1132 } 1133 if (options.index > this.getCurrentNavPathInfo().index) { 1134 this.errorWithCodeAndMsg(POP_PATH_INDEX_OUT_OF_RANGE_ERROR, options); 1135 return; 1136 } 1137 this.navPathStack?.popToIndex(options.index, options.result, options.animated); 1138 } else if (options.delta || options.delta === 0) { 1139 if (!this.checkNumberParamPositive('delta', options.delta, options)) { 1140 return; 1141 } 1142 this.navPathStack?.popToIndex(this.getNavPathIndexByDelta(options.delta), options.result, options.animated); 1143 } else { 1144 this.navPathStack?.pop(options.result, options.animated); 1145 } 1146 let navPathInfo: NavPathInfo = this.getCurrentNavPathInfo(); 1147 this.success(new PopPathResult(navPathInfo.name, navPathInfo.index, navPathInfo.param), options); 1148 } 1149 1150 clearPath(options: ClearPathOptions): void { 1151 if (!this.checkNavPathStack('navPathStack.clear', options)) { 1152 return; 1153 } 1154 this.navPathStack?.clear(options.animated); 1155 this.success(new ClearPathResult(), options); 1156 } 1157 1158 postMessage(options: PostMessageOptions): void { 1159 options.data && this.messageDataList.push(options.data); 1160 this.success(new PostMessageResult(), options); 1161 } 1162 1163 getEnv(options: GetEnvOptions): void { 1164 let res: GetEnvResult = new GetEnvResult(); 1165 res.deviceType = deviceInfo.deviceType; 1166 res.brand = deviceInfo.brand; 1167 res.productModel = deviceInfo.productModel; 1168 res.osFullName = deviceInfo.osFullName; 1169 this.success(res, options); 1170 } 1171 1172 checkJsApi(options: CheckJsApiOptions): void { 1173 let res: Map<string, boolean> = new Map(); 1174 options.jsApiList?.forEach(jsApi => { 1175 res[jsApi] = this.isJsApiEnable(ATOMIC_SERVICE_JS_API_MAP.get(jsApi)); 1176 }); 1177 this.success(new CheckJsApiResult(res), options); 1178 } 1179 1180 pickCamera(options: PickCameraOptions): void { 1181 picker.pick(this.context, options.mediaTypes as Array<picker.PickerMediaType>, { 1182 cameraPosition: options.cameraPosition, 1183 saveUri: options.saveUri, 1184 videoDuration: options.videoDuration 1185 }).then((pickerResult: picker.PickerResult) => { 1186 this.success(new PickCameraResult(pickerResult.resultCode, pickerResult.resultUri, pickerResult.mediaType), 1187 options); 1188 }).catch((err: BusinessError) => { 1189 this.error(err, options); 1190 }); 1191 } 1192 1193 selectPhoto(options: SelectPhotoOptions): void { 1194 let photoViewPicker = new photoAccessHelper.PhotoViewPicker(); 1195 photoViewPicker.select({ 1196 MIMEType: options.mimeType as photoAccessHelper.PhotoViewMIMETypes, 1197 maxSelectNumber: options.maxSelectNumber, 1198 isPhotoTakingSupported: options.isPhotoTakingSupported, 1199 isEditSupported: options.isEditSupported, 1200 isSearchSupported: options.isSearchSupported, 1201 recommendationOptions: { 1202 recommendationType: options.recommendationType 1203 }, 1204 preselectedUris: options.preselectedUris 1205 }).then((selectResult: photoAccessHelper.PhotoSelectResult) => { 1206 this.success(new SelectPhotoResult(selectResult.photoUris, selectResult.isOriginalPhoto), options); 1207 }).catch((err: BusinessError) => { 1208 this.error(err, options); 1209 }); 1210 } 1211 1212 openPreview(options: OpenPreviewOptions): void { 1213 let uri: string = this.getUri(options.uri as string); 1214 filePreview.openPreview(this.context, { 1215 uri: uri, 1216 mimeType: options.mimeType as string, 1217 title: options.title 1218 }).then(() => { 1219 this.success(new OpenPreviewResult(), options); 1220 }).catch((err: BusinessError) => { 1221 this.error(err, options); 1222 }); 1223 } 1224 1225 uploadFile(options: UploadFileOptions): void { 1226 this.checkUploadFile(options).then(res => { 1227 if (!res.checkResult) { 1228 return; 1229 } 1230 let uploadConfig: request.UploadConfig = { 1231 url: options.url as string, 1232 header: options.header as object, 1233 method: options.method as string, 1234 files: this.convertToFile(options.files), 1235 data: this.convertToRequestData(options.data) 1236 }; 1237 request.uploadFile(this.context, uploadConfig).then((uploadTask: request.UploadTask) => { 1238 uploadTask.on('complete', (taskStateArray: Array<request.TaskState>) => { 1239 this.handleUploadFileResult(taskStateArray, res.uriMap as Map<string, string>, options); 1240 }); 1241 uploadTask.on('fail', (taskStateArray: Array<request.TaskState>) => { 1242 this.handleUploadFileResult(taskStateArray, res.uriMap as Map<string, string>, options); 1243 }); 1244 }).catch((err: BusinessError) => { 1245 this.error(err, options); 1246 }); 1247 }).catch((err: BusinessError) => { 1248 this.error(err, options); 1249 }); 1250 } 1251 1252 downloadFile(options: DownloadFileOptions): void { 1253 let cacheFileName: string = `${util.generateRandomUUID().replaceAll('-', '')}`; 1254 let filePath: string = `${this.context.cacheDir}/${cacheFileName}`; 1255 request.downloadFile(this.context, { 1256 url: options.url, 1257 header: options.header ? options.header : new Object(), 1258 filePath: filePath, 1259 enableMetered: options.enableMetered, 1260 enableRoaming: options.enableRoaming, 1261 networkType: options.networkType, 1262 background: false 1263 }).then((downloadTask: request.DownloadTask) => { 1264 downloadTask.on('complete', () => { 1265 this.success(new DownloadFileResult(filePath), options); 1266 }); 1267 downloadTask.on('fail', errCode => { 1268 this.errorWithCodeAndMsg(new AsError(errCode, 'File download fail.'), options); 1269 }); 1270 }).catch((err: BusinessError) => { 1271 this.error(err, options); 1272 }); 1273 } 1274 1275 getNetworkType(options: GetNetworkTypeOptions): void { 1276 connection.getDefaultNet().then(netHandle => { 1277 if (!netHandle || netHandle.netId === 0) { 1278 this.errorWithCodeAndMsg(NETWORK_NO_ACTIVE_ERROR, options); 1279 return; 1280 } 1281 connection.getNetCapabilities(netHandle).then(netCapabilities => { 1282 let res: GetNetworkTypeResult = new GetNetworkTypeResult(netCapabilities.bearerTypes, 1283 netCapabilities.networkCap, netCapabilities.linkUpBandwidthKbps, netCapabilities.linkDownBandwidthKbps); 1284 this.success(res, options); 1285 }).catch((err: BusinessError) => { 1286 this.error(err, options); 1287 }); 1288 }).catch((err: BusinessError) => { 1289 this.error(err, options); 1290 }); 1291 } 1292 1293 getLocation(options: GetLocationOptions): void { 1294 this.checkPermissions(PERMISSION_APPROXIMATELY_LOCATION, err => { 1295 if (err) { 1296 this.error(err, options); 1297 return; 1298 } 1299 geoLocationManager.getCurrentLocation({ 1300 priority: options.priority, 1301 scenario: options.scenario, 1302 maxAccuracy: options.maxAccuracy, 1303 timeoutMs: options.timeoutMs 1304 }).then(location => { 1305 let res: GetLocationResult = new GetLocationResult(location.latitude, location.longitude, location.altitude, 1306 location.accuracy, location.speed, location.timeStamp, location.direction, location.timeSinceBoot, 1307 location.additions, location.additionSize); 1308 this.success(res, options); 1309 }).catch((err: BusinessError) => { 1310 this.error(err, options); 1311 }); 1312 }); 1313 } 1314 1315 /** 1316 * 静默登录 1317 * 1318 * @param options 接口参数 1319 */ 1320 public login(options: LoginOptions): void { 1321 let loginRequest: authentication.LoginWithHuaweiIDRequest = 1322 new authentication.HuaweiIDProvider().createLoginWithHuaweiIDRequest(); 1323 loginRequest.forceLogin = false; 1324 loginRequest.state = util.generateRandomUUID(); 1325 let controller: authentication.AuthenticationController = new authentication.AuthenticationController(); 1326 controller.executeRequest(loginRequest).then((response: authentication.LoginWithHuaweiIDResponse) => { 1327 if (!response || !response.data) { 1328 this.errorWithCodeAndMsg(LOGIN_RESPONSE_DATA_NULL_ERROR, options); 1329 return; 1330 } 1331 if (loginRequest.state !== response.state) { 1332 this.errorWithCodeAndMsg(LOGIN_STATE_INVALID_ERROR, options); 1333 return; 1334 } 1335 this.success(new LoginResult(response.data.openID, response.data.unionID, response.data.authorizationCode, 1336 response.data.idToken), options); 1337 }).catch((err: BusinessError) => { 1338 this.error(err, options); 1339 }); 1340 } 1341 1342 /** 1343 * 请求拉起华为支付 1344 * 1345 * @param options 接口参数 1346 */ 1347 public requestPayment(options: RequestPaymentOptions): void { 1348 if (typeof options.orderStr !== 'string') { 1349 this.errorWithCodeAndMsg(REQUEST_PAYMENT_ORDER_STR_INVALID_ERROR, options); 1350 return; 1351 } 1352 paymentService.requestPayment(this.context, options.orderStr).then(() => { 1353 this.success(new RequestPaymentResult(), options); 1354 }).catch((err: BusinessError) => { 1355 this.error(err, options); 1356 }); 1357 } 1358} 1359 1360class NavPathInfo { 1361 public name: string | undefined; 1362 public index: number; 1363 public param?: object; 1364 1365 constructor(name: string | undefined, index: number) { 1366 this.name = name; 1367 this.index = index; 1368 } 1369} 1370 1371class CheckUploadFileResult { 1372 public checkResult: boolean; 1373 public uriMap?: Map<string, string>; 1374 1375 constructor(checkResult: boolean, uriMap?: Map<string, string>) { 1376 this.checkResult = checkResult; 1377 this.uriMap = uriMap; 1378 } 1379} 1380 1381class BaseOptions<T> { 1382 public callback?: (err: AsError | undefined, res?: T) => void; 1383 public apiNameAlias?: string; 1384} 1385 1386class PushUrlOptions extends BaseOptions<PushUrlResult> { 1387 public url?: string; 1388 public params?: object; 1389 public mode?: string; 1390} 1391 1392class PushUrlResult { 1393} 1394 1395class ReplaceUrlOptions extends BaseOptions<ReplaceUrlResult> { 1396 public url?: string; 1397 public params?: object; 1398 public mode?: string; 1399} 1400 1401class ReplaceUrlResult { 1402} 1403 1404class BackUrlOptions extends BaseOptions<BackUrlResult> { 1405 public url?: string; 1406 public index?: number; 1407 public delta?: number; 1408 public params?: object; 1409} 1410 1411class BackUrlResult { 1412} 1413 1414class ClearUrlOptions extends BaseOptions<ClearUrlResult> { 1415} 1416 1417class ClearUrlResult { 1418} 1419 1420class OnPopEvent { 1421 public name?: string; 1422 public param?: object; 1423 public result?: object; 1424 1425 constructor(name?: string, param?: object, result?: object) { 1426 this.name = name; 1427 this.param = param; 1428 this.result = result; 1429 } 1430} 1431 1432class PushPathOptions extends BaseOptions<PushPathResult> { 1433 public name?: string; 1434 public param?: object; 1435 public animated?: boolean; 1436 public onPop?: (event: OnPopEvent) => void; 1437} 1438 1439class PushPathResult { 1440} 1441 1442class ReplacePathOptions extends BaseOptions<ReplacePathResult> { 1443 public name?: string; 1444 public param?: object; 1445 public animated?: boolean; 1446 public onPop?: (event: OnPopEvent) => void; 1447} 1448 1449class ReplacePathResult { 1450} 1451 1452class PopPathOptions extends BaseOptions<PopPathResult> { 1453 public name?: string; 1454 public index?: number; 1455 public delta?: number; 1456 public result?: object; 1457 public animated?: boolean; 1458} 1459 1460class PopPathResult { 1461 public name: string | undefined; 1462 public index: number; 1463 public param?: object; 1464 1465 constructor(name: string | undefined, index: number, param?: object) { 1466 this.name = name; 1467 this.index = index; 1468 this.param = param; 1469 } 1470} 1471 1472class ClearPathOptions extends BaseOptions<ClearPathResult> { 1473 public animated?: boolean; 1474} 1475 1476class ClearPathResult { 1477} 1478 1479class PostMessageOptions extends BaseOptions<PostMessageResult> { 1480 public data?: object; 1481} 1482 1483class PostMessageResult { 1484} 1485 1486export class OnMessageEvent { 1487 public data: object[]; 1488 1489 constructor(data: object[]) { 1490 this.data = data; 1491 } 1492} 1493 1494export class OnErrorReceiveEvent { 1495 public request: WebResourceRequest; 1496 public error: WebResourceError; 1497 1498 constructor(request: WebResourceRequest, error: WebResourceError) { 1499 this.request = request; 1500 this.error = error; 1501 } 1502} 1503 1504export class OnHttpErrorReceiveEvent { 1505 public request: WebResourceRequest; 1506 public response: WebResourceResponse; 1507 1508 constructor(request: WebResourceRequest, response: WebResourceResponse) { 1509 this.request = request; 1510 this.response = response; 1511 } 1512} 1513 1514export class OnPageBeginEvent { 1515 public url: string; 1516 1517 constructor(url: string) { 1518 this.url = url; 1519 } 1520} 1521 1522export class OnPageEndEvent { 1523 public url: string; 1524 1525 constructor(url: string) { 1526 this.url = url; 1527 } 1528} 1529 1530export class WebHeader { 1531 public headerKey: string; 1532 public headerValue: string; 1533 1534 constructor(headerKey: string, headerValue: string) { 1535 this.headerKey = headerKey; 1536 this.headerValue = headerValue; 1537 } 1538} 1539 1540class GetEnvOptions extends BaseOptions<GetEnvResult> { 1541} 1542 1543class GetEnvResult { 1544 public deviceType?: string; 1545 public brand?: string; 1546 public productModel?: string; 1547 public osFullName?: string; 1548} 1549 1550class CheckJsApiOptions extends BaseOptions<CheckJsApiResult> { 1551 public jsApiList?: string[]; 1552} 1553 1554class CheckJsApiResult { 1555 public checkResult?: Map<string, boolean>; 1556 1557 constructor(checkResult?: Map<string, boolean>) { 1558 this.checkResult = checkResult; 1559 } 1560} 1561 1562class PickCameraOptions extends BaseOptions<PickCameraResult> { 1563 public mediaTypes?: string[]; 1564 public cameraPosition?: number; 1565 public saveUri?: string; 1566 public videoDuration?: number; 1567} 1568 1569class PickCameraResult { 1570 public resultCode?: number; 1571 public resultUri?: string; 1572 public mediaType?: string; 1573 1574 constructor(resultCode?: number, resultUri?: string, mediaType?: string) { 1575 this.resultCode = resultCode; 1576 this.resultUri = resultUri; 1577 this.mediaType = mediaType; 1578 } 1579} 1580 1581class SelectPhotoOptions extends BaseOptions<SelectPhotoResult> { 1582 public mimeType?: string; 1583 public maxSelectNumber?: number; 1584 public isPhotoTakingSupported?: boolean; 1585 public isEditSupported?: boolean; 1586 public isSearchSupported?: boolean; 1587 public recommendationType?: number; 1588 public preselectedUris?: string[]; 1589} 1590 1591class SelectPhotoResult { 1592 public photoUris?: string[]; 1593 public isOriginalPhoto?: boolean; 1594 1595 constructor(photoUris?: string[], isOriginalPhoto?: boolean) { 1596 this.photoUris = photoUris; 1597 this.isOriginalPhoto = isOriginalPhoto; 1598 } 1599} 1600 1601class OpenPreviewOptions extends BaseOptions<OpenPreviewResult> { 1602 public title?: string; 1603 public uri?: string; 1604 public mimeType?: string; 1605} 1606 1607class OpenPreviewResult { 1608} 1609 1610class UploadFileOptions extends BaseOptions<UploadFileResult> { 1611 public url?: string; 1612 public header?: object; 1613 public method?: string; 1614 public files?: UploadFile[]; 1615 public data?: UploadRequestData[]; 1616} 1617 1618class UploadFile { 1619 public filename: string; 1620 public name: string; 1621 public uri: string; 1622 public type: string; 1623 1624 constructor(filename: string, name: string, uri: string, type: string) { 1625 this.filename = filename; 1626 this.name = name; 1627 this.uri = uri; 1628 this.type = type; 1629 } 1630} 1631 1632class UploadRequestData { 1633 public name?: string; 1634 public value?: string; 1635} 1636 1637class UploadFileResult { 1638 public taskStates?: UploadFileTaskState[]; 1639 1640 constructor(taskStates?: UploadFileTaskState[]) { 1641 this.taskStates = taskStates; 1642 } 1643} 1644 1645class UploadFileTaskState { 1646 public path?: string; 1647 public responseCode?: number; 1648 public message?: string; 1649 1650 constructor(path?: string, responseCode?: number, message?: string) { 1651 this.path = path; 1652 this.responseCode = responseCode; 1653 this.message = message; 1654 } 1655} 1656 1657class DownloadFileOptions extends BaseOptions<DownloadFileResult> { 1658 public url?: string; 1659 public header?: object; 1660 public fileName?: string; 1661 public enableMetered?: boolean; 1662 public enableRoaming?: boolean; 1663 public networkType?: number; 1664} 1665 1666class DownloadFileResult { 1667 public uri?: string; 1668 1669 constructor(uri?: string) { 1670 this.uri = uri; 1671 } 1672} 1673 1674class GetNetworkTypeOptions extends BaseOptions<GetNetworkTypeResult> { 1675} 1676 1677class GetNetworkTypeResult { 1678 public bearerTypes: number[]; 1679 public networkCap?: number[]; 1680 public linkUpBandwidthKbps?: number; 1681 public linkDownBandwidthKbps?: number; 1682 1683 constructor(bearerTypes: number[], networkCap?: number[], linkUpBandwidthKbps?: number, 1684 linkDownBandwidthKbps?: number) { 1685 this.bearerTypes = bearerTypes; 1686 this.networkCap = networkCap; 1687 this.linkUpBandwidthKbps = linkUpBandwidthKbps; 1688 this.linkDownBandwidthKbps = linkDownBandwidthKbps; 1689 } 1690} 1691 1692class GetLocationOptions extends BaseOptions<GetLocationResult> { 1693 public priority?: number; 1694 public scenario?: number; 1695 public maxAccuracy?: number; 1696 public timeoutMs?: number; 1697} 1698 1699class GetLocationResult { 1700 public latitude: number; 1701 public longitude: number; 1702 public altitude: number; 1703 public accuracy: number; 1704 public speed: number; 1705 public timeStamp: number; 1706 public direction: number; 1707 public timeSinceBoot: number; 1708 public additions?: string[] | undefined; 1709 public additionSize?: number; 1710 1711 constructor(latitude: number, longitude: number, altitude: number, accuracy: number, speed: number, 1712 timeStamp: number, direction: number, timeSinceBoot: number, additions?: string[], additionSize?: number) { 1713 this.latitude = latitude; 1714 this.longitude = longitude; 1715 this.altitude = altitude; 1716 this.accuracy = accuracy; 1717 this.speed = speed; 1718 this.timeStamp = timeStamp; 1719 this.direction = direction; 1720 this.timeSinceBoot = timeSinceBoot; 1721 this.additions = additions; 1722 this.additionSize = additionSize; 1723 } 1724} 1725 1726/** 1727 * login接口参数 1728 */ 1729class LoginOptions extends BaseOptions<LoginResult> { 1730} 1731 1732/** 1733 * login接口结果 1734 */ 1735class LoginResult { 1736 public code?: string; 1737 public idToken?: string; 1738 public openID: string; 1739 public unionID: string; 1740 1741 constructor(openID: string, unionID: string, code?: string, idToken?: string) { 1742 this.code = code; 1743 this.idToken = idToken; 1744 this.openID = openID; 1745 this.unionID = unionID; 1746 } 1747} 1748 1749/** 1750 * requestPayment接口参数 1751 */ 1752class RequestPaymentOptions extends BaseOptions<RequestPaymentResult> { 1753 public orderStr?: string; 1754} 1755 1756/** 1757 * requestPayment接口结果 1758 */ 1759class RequestPaymentResult { 1760} 1761 1762/** 1763 * 大数据打点工具类 1764 */ 1765class HiAnalyticsUtil { 1766 private static appIdentifier?: string; 1767 private static runningMode?: string; 1768 private static processorId: number | undefined = undefined; 1769 /** 1770 * 打点数据上报 1771 * 1772 * @param time 时间戳 1773 * @param content 打点参数json字符串 1774 */ 1775 private static writeEndEvent(time: number, content: string): void { 1776 console.info(`writeEndEvent -> time: ${time} ,content: ${content} `); 1777 let event: hiAppEvent.AppEventInfo = { 1778 domain: 'api_diagnostic', 1779 name: 'api_exec_end', 1780 params: { 1781 api_name: 'ascf', 1782 sdk_name: 'atomicservice_web', 1783 begin_time: time, 1784 // 调用次数 1785 call_times: 3, 1786 // 调用成功次数 1787 success_times: 1, 1788 contents: content 1789 }, 1790 eventType: hiAppEvent.EventType.BEHAVIOR 1791 }; 1792 hiAppEvent.write(event); 1793 } 1794 1795 /** 1796 * 应用的唯一标识 1797 */ 1798 private static initAnalytics(): void { 1799 if (!HiAnalyticsUtil.processorId) { 1800 HiAnalyticsUtil.addEventProcessor(); 1801 } 1802 if (HiAnalyticsUtil.appIdentifier && HiAnalyticsUtil.runningMode) { 1803 return; 1804 } 1805 let bundleInfo: bundleManager.BundleInfo = 1806 bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_SIGNATURE_INFO | 1807 bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION); 1808 HiAnalyticsUtil.appIdentifier = bundleInfo.signatureInfo.appIdentifier; 1809 HiAnalyticsUtil.runningMode = bundleInfo.appInfo.appProvisionType; 1810 } 1811 1812 1813 /** 1814 * 添加处理者 1815 */ 1816 private static addEventProcessor(): number { 1817 let processor: hiAppEvent.Processor = { 1818 name: 'ha_app_event', 1819 appId: 'com_huawei_hmos_sdk_ocg', 1820 routeInfo: 'AUTO', 1821 eventConfigs: [ 1822 { 1823 domain: 'api_diagnostic', 1824 name: 'api_exec_end', 1825 isRealTime: false 1826 }, 1827 { 1828 domain: 'api_diagnostic', 1829 name: 'api_called_stat', 1830 isRealTime: true 1831 }, 1832 { 1833 domain: 'api_diagnostic', 1834 name: 'api_called_stat_cnt', 1835 isRealTime: true 1836 }, 1837 ], 1838 periodReport: 90, 1839 batchReport: 30 1840 }; 1841 return hiAppEvent.addProcessor(processor); 1842 } 1843 1844 /** 1845 * asweb jssdk API数据打点业务处理 1846 * 1847 * @param apiName 接口名 1848 * @param actionState 接口调用状态 1849 * @param errorNumber 错误码 1850 */ 1851 public static reportApiEvent(apiName: string, actionState: string, errorNumber?: number): void { 1852 try { 1853 if (!apiName || !actionState) { 1854 return; 1855 } 1856 HiAnalyticsUtil.initAnalytics(); 1857 const ascfAction: string = 'APICaller_' + apiName; 1858 const content: string = JSON.stringify({ 1859 appIdentify: HiAnalyticsUtil.appIdentifier, // 获取元服务appid 1860 ascfVersionName: deviceInfo.displayVersion, // rom版本号 1861 runningMode: HiAnalyticsUtil.runningMode, 1862 ascfAction, 1863 caller: 'ASWeb', 1864 actionState, 1865 errorNumber 1866 }); 1867 HiAnalyticsUtil.writeEndEvent(new Date().getTime(), content); 1868 } catch (err) { 1869 console.error(`reportApiEvent -> reportApiEvent error, message: ${err.message}`); 1870 } 1871 } 1872 /** 1873 * asweb 组件创建打点 1874 * 1875 * @param apiName 接口名 1876 * @param actionState 接口调用状态 1877 * @param errorNumber 错误码 1878 */ 1879 public static reportComponentEvent(): void { 1880 try { 1881 HiAnalyticsUtil.initAnalytics(); 1882 const content: string = JSON.stringify({ 1883 appIdentify: HiAnalyticsUtil.appIdentifier, // 获取元服务appid 1884 ascfVersionName: deviceInfo.displayVersion, // rom版本号 1885 runningMode: HiAnalyticsUtil.runningMode, 1886 caller: 'ASWeb', 1887 }); 1888 HiAnalyticsUtil.writeEndEvent(new Date().getTime(), content); 1889 } catch (err) { 1890 console.error(`reportComponentEvent -> reportComponentEvent error, message: ${err.message}`); 1891 } 1892 } 1893} 1894 1895/** 1896 * 数据打点的接口状态 1897 */ 1898enum ActionState { 1899 BEGIN = 'begin', 1900 SUCCESS = 'success', 1901 FAIL = 'fail' 1902} 1903