1/* 2 * Copyright (c) 2023 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 16let cert = requireInternal('security.cert'); 17let webview = requireInternal('web.webview'); 18let fileIo = requireNapi('file.fs'); 19let fileUri = requireNapi('file.fileuri'); 20let picker = requireNapi('file.picker'); 21let photoAccessHelper = requireNapi('file.photoAccessHelper'); 22let cameraPicker = requireNapi('multimedia.cameraPicker'); 23let camera = requireNapi('multimedia.camera'); 24let accessControl = requireNapi('abilityAccessCtrl'); 25let deviceinfo = requireInternal('deviceInfo'); 26let promptAction = requireNapi('promptAction'); 27const PARAM_CHECK_ERROR = 401; 28 29const ERROR_MSG_INVALID_PARAM = 'Invalid input parameter'; 30 31let errMsgMap = new Map(); 32errMsgMap.set(PARAM_CHECK_ERROR, ERROR_MSG_INVALID_PARAM); 33let customDialogComponentId = 0; 34 35class BusinessError extends Error { 36 constructor(code, errorMsg = 'undefined') { 37 if (errorMsg === 'undefined') { 38 let msg = errMsgMap.get(code); 39 super(msg); 40 } else { 41 super(errorMsg); 42 } 43 this.code = code; 44 } 45} 46 47function getCertificatePromise(certChainData) { 48 let x509CertArray = []; 49 if (!(certChainData instanceof Array)) { 50 console.log('failed, cert chain data type is not array'); 51 return Promise.all(x509CertArray); 52 } 53 54 for (let i = 0; i < certChainData.length; i++) { 55 let encodeBlobData = { 56 data: certChainData[i], 57 encodingFormat: cert.EncodingFormat.FORMAT_DER 58 }; 59 x509CertArray[i] = cert.createX509Cert(encodeBlobData); 60 } 61 62 return Promise.all(x509CertArray); 63} 64 65function takePhoto(param, selectResult) { 66 try { 67 let pickerProfileOptions = { 68 'cameraPosition': camera.CameraPosition.CAMERA_POSITION_BACK, 69 }; 70 let acceptTypes = param.getAcceptType(); 71 let mediaType = []; 72 if (isContainImageMimeType(acceptTypes)) { 73 mediaType.push(cameraPicker.PickerMediaType.PHOTO); 74 } 75 if (isContainVideoMimeType(acceptTypes)) { 76 mediaType.push(cameraPicker.PickerMediaType.VIDEO); 77 } 78 cameraPicker.pick(getContext(this), mediaType, pickerProfileOptions) 79 .then((pickerResult) => { 80 selectResult.handleFileList([pickerResult.resultUri]); 81 }).catch((error) => { 82 console.log('selectFile error:' + JSON.stringify(error)); 83 throw error; 84 }); 85 86 } catch (error) { 87 console.log('the pick call failed, error code' + JSON.stringify(error)); 88 selectResult.handleFileList([]); 89 promptAction.showToast({ message: '无法打开拍照功能,请检查是否具备拍照功能' }); 90 } 91} 92 93function needShowDialog(params) { 94 let result = false; 95 try { 96 if (params.isCapture()) { 97 console.log('input element contain capture tag, not show dialog'); 98 return false; 99 } 100 let acceptTypes = params.getAcceptType(); 101 if (isContainImageMimeType(acceptTypes) || isContainVideoMimeType(acceptTypes)) { 102 result = true; 103 } 104 } catch (error) { 105 console.log('show dialog happend error:' + JSON.stringify(error)); 106 } 107 return result; 108} 109 110function selectFile(param, result) { 111 try { 112 let documentPicker = new picker.DocumentViewPicker(); 113 if (param.getMode() !== FileSelectorMode.FileSaveMode) { 114 documentPicker.select(createDocumentSelectionOptions(param)) 115 .then((documentSelectResult) => { 116 let filePath = documentSelectResult; 117 result.handleFileList(filePath); 118 }).catch((error) => { 119 console.log('selectFile error: ' + JSON.stringify(error)); 120 throw error; 121 }); 122 } else { 123 documentPicker.save(createDocumentSaveOptions(param)) 124 .then((documentSaveResult) => { 125 let filePaths = documentSaveResult; 126 let tempUri = ''; 127 if (filePaths.length > 0) { 128 let fileName = filePaths[0].substr(filePaths[0].lastIndexOf('/')); 129 let tempPath = getContext(this).filesDir + fileName; 130 tempUri = fileUri.getUriFromPath(tempPath); 131 let randomAccessFile = fileIo.createRandomAccessFileSync(tempPath, fileIo.OpenMode.CREATE); 132 randomAccessFile.close(); 133 134 let watcher = fileIo.createWatcher(tempPath, 0x4, () => { 135 fileIo.copy(tempUri, filePaths[0]).then(() => { 136 console.log('Web save file succeeded in copying.'); 137 fileIo.unlink(tempPath); 138 }).catch((err) => { 139 console.error(`Web save file failed to copy: ${JSON.stringify(err)}.`); 140 }).finally(() => { 141 watcher.stop(); 142 }); 143 }); 144 watcher.start(); 145 } 146 result.handleFileList([tempUri]); 147 }).catch((error) => { 148 console.log('saveFile error: ' + JSON.stringify(error)); 149 throw error; 150 }); 151 } 152 } catch (error) { 153 console.log('picker error: ' + JSON.stringify(error)); 154 result.handleFileList([]); 155 promptAction.showToast({ message: '无法打开文件功能,请检查是否具备文件功能' }); 156 } 157} 158 159function createDocumentSelectionOptions(param) { 160 let documentSelectOptions = new picker.DocumentSelectOptions(); 161 let currentDevice = deviceinfo.deviceType.toLowerCase(); 162 try { 163 let defaultSelectNumber = 500; 164 let defaultSelectMode = picker.DocumentSelectMode.MIXED; 165 documentSelectOptions.maxSelectNumber = defaultSelectNumber; 166 documentSelectOptions.selectMode = defaultSelectMode; 167 let mode = param.getMode(); 168 switch (mode) { 169 case FileSelectorMode.FileOpenMode: 170 documentSelectOptions.maxSelectNumber = 1; 171 documentSelectOptions.selectMode = picker.DocumentSelectMode.FILE; 172 break; 173 case FileSelectorMode.FileOpenMultipleMode: 174 documentSelectOptions.selectMode = picker.DocumentSelectMode.FILE; 175 break; 176 case FileSelectorMode.FileOpenFolderMode: 177 documentSelectOptions.selectMode = picker.DocumentSelectMode.FOLDER; 178 break; 179 default: 180 break; 181 } 182 documentSelectOptions.fileSuffixFilters = []; 183 let suffix = param.getAcceptType().join(','); 184 if (suffix) { 185 documentSelectOptions.fileSuffixFilters.push(suffix); 186 } 187 if (currentDevice !== 'phone') { 188 documentSelectOptions.fileSuffixFilters.push('.*'); 189 } 190 } catch (error) { 191 console.log('selectFile error: ' + + JSON.stringify(error)); 192 } 193 return documentSelectOptions; 194} 195 196function createDocumentSaveOptions(param) { 197 let documentSaveOptions = new picker.DocumentSaveOptions(); 198 let currentDevice = deviceinfo.deviceType.toLowerCase(); 199 try { 200 documentSaveOptions.pickerMode = picker.DocumentPickerMode.DEFAULT; 201 documentSaveOptions.fileSuffixChoices = []; 202 let suffix = param.getAcceptType().join(','); 203 if (suffix) { 204 documentSaveOptions.fileSuffixChoices.push(suffix); 205 } 206 if (currentDevice !== 'phone') { 207 documentSaveOptions.fileSuffixChoices.push('.*'); 208 } 209 } catch (error) { 210 console.log('saveFile error: ' + + JSON.stringify(error)); 211 } 212 return documentSaveOptions; 213} 214 215function isContainImageMimeType(acceptTypes) { 216 if (!(acceptTypes instanceof Array)) { 217 return false; 218 } 219 if (acceptTypes.length < 1) { 220 return true; 221 } 222 223 let imageTypes = ['tif', 'xbm', 'tiff', 'pjp', 'jfif', 'bmp', 'avif', 'apng', 'ico', 224 'webp', 'svg', 'gif', 'svgz', 'jpg', 'jpeg', 'png', 'pjpeg']; 225 for (let i = 0; i < acceptTypes.length; i++) { 226 for (let j = 0; j < imageTypes.length; j++) { 227 if (acceptTypes[i].includes(imageTypes[j])) { 228 return true; 229 } 230 } 231 } 232 return false; 233} 234 235function isContainVideoMimeType(acceptTypes) { 236 if (!(acceptTypes instanceof Array)) { 237 return false; 238 } 239 if (acceptTypes.length < 1) { 240 return true; 241 } 242 243 let videoTypes = ['ogm', 'ogv', 'mpg', 'mp4', 'mpeg', 'm4v', 'webm']; 244 for (let i = 0; i < acceptTypes.length; i++) { 245 for (let j = 0; j < videoTypes.length; j++) { 246 if (acceptTypes[i].includes(videoTypes[j])) { 247 return true; 248 } 249 } 250 } 251 return false; 252} 253 254function fileSelectorListItem(callback, sysResource, text, func) { 255 const itemCreation = (elmtId, isInitialRender) => { 256 ViewStackProcessor.StartGetAccessRecordingFor(elmtId); 257 itemCreation2(elmtId, isInitialRender); 258 if (!isInitialRender) { 259 ListItem.pop(); 260 } 261 ViewStackProcessor.StopGetAccessRecording(); 262 }; 263 const itemCreation2 = (elmtId, isInitialRender) => { 264 ListItem.create(deepRenderFunction, true); 265 ListItem.onClick(() => { 266 promptAction.closeCustomDialog(customDialogComponentId); 267 func(callback.fileparam, callback.fileresult); 268 }); 269 ListItem.height(48); 270 ListItem.padding({ 271 left: 24, 272 right: 24 273 }); 274 }; 275 const deepRenderFunction = (elmtId, isInitialRender) => { 276 itemCreation(elmtId, isInitialRender); 277 Row.create(); 278 SymbolGlyph.create({ 'id': -1, 'type': -1, params: [sysResource], 'bundleName': 'com.example.selectdialog', 'moduleName': 'entry' }); 279 SymbolGlyph.width(24); 280 SymbolGlyph.height(24); 281 SymbolGlyph.fontSize(24); 282 SymbolGlyph.margin({ 283 right: 16 284 }); 285 Row.create(); 286 Row.width(deviceinfo.deviceType.toLowerCase() === '2in1' ? 312 : 240); 287 Row.border({ width: { bottom: 0.5 }, color: '#33000000' }); 288 Text.create(text); 289 Text.fontSize(16); 290 Text.fontWeight(FontWeight.Medium); 291 Text.lineHeight(19); 292 Text.margin({ 293 top: 13, 294 bottom: 13 295 }); 296 Text.pop(); 297 Row.pop(); 298 Row.pop(); 299 ListItem.pop(); 300 }; 301 itemCreation(ViewStackProcessor.AllocateNewElmetIdForNextComponent(), true); 302 ListItem.pop(); 303} 304 305function fileSelectorDialog(callback) { 306 Row.create(); 307 Row.height(56); 308 Text.create('选择上传'); 309 Text.fontSize(20); 310 Text.fontWeight(FontWeight.Bold); 311 Text.lineHeight(23); 312 Text.margin({ 313 top: 15, 314 bottom: 15, 315 left: 24, 316 right: 24, 317 }); 318 Text.pop(); 319 Row.pop(); 320 List.create(); 321 List.width('100%'); 322 fileSelectorListItem(callback, 'sys.symbol.picture', '照片', selectPicture); 323 fileSelectorListItem(callback, 'sys.symbol.camera', '拍照', takePhoto); 324 fileSelectorListItem(callback, 'sys.symbol.doc_text', '文件', selectFile); 325 List.pop(); 326} 327 328function fileSelectorDialogForPC(callback) { 329 Column.create(); 330 Column.height(272); 331 Column.width(400); 332 fileSelectorDialog(callback); 333 Row.create(); 334 Row.onClick(() => { 335 try { 336 console.log('Get Alert Dialog handled'); 337 callback.fileresult.handleFileList([]); 338 promptAction.closeCustomDialog(customDialogComponentId); 339 } 340 catch (error) { 341 let message = error.message; 342 let code = error.code; 343 console.error(`closeCustomDialog error code is ${code}, message is ${message}`); 344 } 345 }); 346 Row.width(368); 347 Row.height(40); 348 Row.margin(16); 349 Row.borderRadius(5); 350 Row.backgroundColor('#ededed'); 351 Row.justifyContent(FlexAlign.Center); 352 Text.create('取消'); 353 Text.fontSize(16); 354 Text.fontColor('#FF0A59F7'); 355 Text.fontWeight(FontWeight.Medium); 356 Text.margin({ 357 top: 10, 358 bottom: 10, 359 left: 16, 360 right: 16 361 }); 362 Text.pop(); 363 Row.pop(); 364 Column.pop(); 365} 366 367function fileSelectorDialogForPhone(callback) { 368 Column.create(); 369 Column.height(264); 370 Column.width(328); 371 fileSelectorDialog(callback); 372 Row.create(); 373 Row.onClick(() => { 374 try { 375 console.log('Get Alert Dialog handled'); 376 callback.fileresult.handleFileList([]); 377 promptAction.closeCustomDialog(customDialogComponentId); 378 } 379 catch (error) { 380 let message = error.message; 381 let code = error.code; 382 console.error(`closeCustomDialog error code is ${code}, message is ${message}`); 383 } 384 }); 385 Row.width(296); 386 Row.height(40); 387 Row.margin({ 388 top: 8, 389 bottom: 16, 390 left: 16, 391 right: 16 392 }); 393 Row.borderRadius(5); 394 Row.justifyContent(FlexAlign.Center); 395 Text.create('取消'); 396 Text.fontSize(16); 397 Text.fontColor('#FF0A59F7'); 398 Text.fontWeight(FontWeight.Medium); 399 Text.margin({ 400 top: 10, 401 bottom: 10, 402 left: 104, 403 right: 104 404 }); 405 Text.pop(); 406 Row.pop(); 407 Column.pop(); 408} 409 410function selectPicture(param, selectResult) { 411 try { 412 let photoResultArray = []; 413 let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); 414 if (param.getMode() === FileSelectorMode.FileOpenMode) { 415 console.log('allow select single photo or video'); 416 photoSelectOptions.maxSelectNumber = 1; 417 } 418 let acceptTypes = param.getAcceptType(); 419 photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE; 420 if (isContainImageMimeType(acceptTypes) && !isContainVideoMimeType(acceptTypes)) { 421 photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; 422 } 423 if (!isContainImageMimeType(acceptTypes) && isContainVideoMimeType(acceptTypes)) { 424 photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE; 425 } 426 427 let photoPicker = new photoAccessHelper.PhotoViewPicker(); 428 photoPicker.select(photoSelectOptions).then((photoSelectResult) => { 429 for (let i = 0; i < photoSelectResult.photoUris.length; i++) { 430 photoResultArray.push(photoSelectResult.photoUris[i]); 431 } 432 selectResult.handleFileList(photoResultArray); 433 }); 434 } catch (error) { 435 console.log('selectPicture error' + JSON.stringify(error)); 436 selectResult.handleFileList([]); 437 promptAction.showToast({ message: '无法打开图片功能,请检查是否具备图片功能' }); 438 } 439} 440 441Object.defineProperty(webview.WebviewController.prototype, 'getCertificate', { 442 value: function (callback) { 443 if (arguments.length !== 0 && arguments.length !== 1) { 444 throw new BusinessError(PARAM_CHECK_ERROR, 445 'BusinessError 401: Parameter error. The number of params must be zero or one.'); 446 } 447 448 let certChainData = this.innerGetCertificate(); 449 if (callback === undefined) { 450 console.log('get certificate promise'); 451 return getCertificatePromise(certChainData); 452 } else { 453 console.log('get certificate async callback'); 454 if (typeof callback !== 'function') { 455 throw new BusinessError(PARAM_CHECK_ERROR, 456 'BusinessError 401: Parameter error. The type of "callback" must be function.'); 457 } 458 return getCertificatePromise(certChainData).then(x509CertArray => { 459 callback(undefined, x509CertArray); 460 }).catch(error => { 461 callback(error, undefined); 462 }); 463 } 464 } 465}); 466 467Object.defineProperty(webview.WebviewController.prototype, 'fileSelectorShowFromUserWeb', { 468 value: function (callback) { 469 let currentDevice = deviceinfo.deviceType.toLowerCase(); 470 if (needShowDialog(callback.fileparam)) { 471 promptAction.openCustomDialog({ 472 builder: () => { 473 if (currentDevice === '2in1') { 474 fileSelectorDialogForPC(callback); 475 } else { 476 fileSelectorDialogForPhone(callback); 477 } 478 }, 479 onWillDismiss: (dismissDialogAction) => { 480 console.info('reason' + JSON.stringify(dismissDialogAction.reason)); 481 console.log('dialog onWillDismiss'); 482 if (dismissDialogAction.reason === DismissReason.PRESS_BACK) { 483 callback.fileresult.handleFileList([]); 484 dismissDialogAction.dismiss(); 485 } 486 if (dismissDialogAction.reason === DismissReason.TOUCH_OUTSIDE) { 487 callback.fileresult.handleFileList([]); 488 dismissDialogAction.dismiss(); 489 } 490 } 491 }).then((dialogId) => { 492 customDialogComponentId = dialogId; 493 }) 494 .catch((error) => { 495 callback.fileresult.handleFileList([]); 496 console.error(`openCustomDialog error code is ${error.code}, message is ${error.message}`); 497 }); 498 } else if (callback.fileparam.isCapture() && 499 (isContainImageMimeType(callback.fileparam.getAcceptType()) || isContainVideoMimeType(callback.fileparam.getAcceptType()))) { 500 console.log('take photo will be directly invoked due to the capture property'); 501 takePhoto(callback.fileparam, callback.fileresult); 502 } else { 503 console.log('selectFile will be invoked by web'); 504 selectFile(callback.fileparam, callback.fileresult); 505 } 506 } 507}); 508 509Object.defineProperty(webview.WebviewController.prototype, 'requestPermissionsFromUserWeb', { 510 value: function (callback) { 511 let accessManger = accessControl.createAtManager(); 512 let abilityContext = getContext(this); 513 accessManger.requestPermissionsFromUser(abilityContext, ['ohos.permission.READ_PASTEBOARD']) 514 .then((PermissionRequestResult) => { 515 if (PermissionRequestResult.authResults[0] === 0) { 516 console.log('requestPermissionsFromUserWeb is allowed'); 517 callback.request.grant(callback.request.getAccessibleResource()); 518 } 519 else { 520 console.log('requestPermissionsFromUserWeb is refused'); 521 callback.request.deny(); 522 } 523 }) 524 .catch((error) => { 525 callback.request.deny(); 526 }); 527 } 528}); 529 530Object.defineProperty(webview.WebviewController.prototype, 'openAppLink', { 531 value: function (callback) { 532 let abilityContext = getContext(this); 533 try { 534 let option = { 535 appLinkingOnly: true 536 }; 537 console.log('begin openAppLink'); 538 abilityContext.openLink(callback.url, option, null) 539 .then(() => { 540 console.log('applink success openLink'); 541 callback.result.cancelLoad(); 542 }) 543 .catch((error) => { 544 console.log(`applink openLink ErrorCode: ${error.code}, Message: ${error.message}`); 545 callback.result.continueLoad(); 546 }); 547 } catch (err) { 548 console.log(`applink openLink ErrorCode: ${err.code}, Message: ${err.message}`); 549 setTimeout(() => { 550 callback.result.continueLoad(); 551 }, 1); 552 } 553 } 554}); 555 556export default webview; 557