From bf6429dc76abb38cd0be5122bb3c61d9b799fcad Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:46:08 +0900 Subject: [PATCH] =?UTF-8?q?Android=20=ED=8C=90=20(APK):=20FFmpeg=20(ffmpeg?= =?UTF-8?q?-kit)=20=EB=A1=9C=20MKV=20=EB=A7=8C=EB=93=A4=EA=B8=B0,=20?= =?UTF-8?q?=EC=95=B1=20=EC=95=88=20=ED=8C=8C=EC=9D=BC=20=EA=B3=A0=EB=A5=B4?= =?UTF-8?q?=EA=B8=B0,=20=EC=A0=80=EC=9E=A5=EC=86=8C=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=20=EA=B6=8C=ED=95=9C,=20Windows=20=EC=A0=84=EC=9A=A9=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=88=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 플러그인 사본: flutter_inappwebview_android (AGP 9 빌드 오류), ffmpeg_kit_flutter_new_min_gpl (Android 만), whisper_ggml (같은 FFmpeg 사용) - 64비트 ARM 만, 배포 키 서명 (android/key.properties, 저장소에 올리지 않음) Co-Authored-By: Claude Opus 5.5 --- .gitignore | 5 + analysis_options.yaml | 4 +- android/app/build.gradle.kts | 49 +- android/app/src/main/AndroidManifest.xml | 12 +- .../kotlin/com/jj/jj_mkvmaker/MainActivity.kt | 48 +- android/build.gradle.kts | 6 + android/gradle.properties | 5 +- lib/main.dart | 62 + lib/platform/android/android_shell.dart | 20 + lib/platform/android/android_storage.dart | 67 + .../android/ffmpeg_kit_media_tool.dart | 114 + lib/services/platform_services.dart | 23 + lib/ui/android_file_browser.dart | 207 ++ lib/ui/settings_page.dart | 18 +- .../CHANGELOG.md | 107 + .../JJ_PATCH.md | 11 + .../ffmpeg_kit_flutter_new_min_gpl/LICENSE | 165 + .../ffmpeg_kit_flutter_new_min_gpl/README.md | 282 ++ .../analysis_options.yaml | 63 + .../android/build.gradle | 56 + .../android/consumer-rules.pro | 17 + .../android/gradle.properties | 3 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + .../android/gradlew | 249 ++ .../android/gradlew.bat | 92 + .../android/proguard-rules.pro | 51 + .../android/settings.gradle | 2 + .../android/src/main/AndroidManifest.xml | 3 + .../FFmpegKitFlutterMethodResultHandler.java | 63 + .../ffmpegkit/FFmpegKitFlutterPlugin.java | 1560 +++++++++ .../ffmpegkit/FFmpegSessionExecuteTask.java | 25 + .../ffmpegkit/FFprobeSessionExecuteTask.java | 26 + .../MediaInformationSessionExecuteTask.java | 28 + .../ffmpegkit/WriteToPipeTask.java | 51 + .../lib/abstract_session.dart | 445 +++ .../lib/arch_detect.dart | 39 + .../lib/chapter.dart | 67 + .../lib/extensions.dart | 26 + .../lib/ffmpeg_kit.dart | 105 + .../lib/ffmpeg_kit_config.dart | 820 +++++ .../lib/ffmpeg_session.dart | 125 + .../lib/ffmpeg_session_complete_callback.dart | 29 + .../lib/ffprobe_kit.dart | 257 ++ .../lib/ffprobe_session.dart | 53 + .../ffprobe_session_complete_callback.dart | 29 + .../lib/level.dart | 84 + .../lib/log.dart | 33 + .../lib/log_callback.dart | 23 + .../lib/log_redirection_strategy.dart | 26 + .../lib/media_information.dart | 125 + .../lib/media_information_json_parser.dart | 64 + .../lib/media_information_session.dart | 64 + ...information_session_complete_callback.dart | 30 + .../lib/packages.dart | 56 + .../lib/return_code.dart | 44 + .../lib/session.dart | 110 + .../lib/session_state.dart | 20 + .../lib/signal.dart | 21 + .../lib/src/ffmpeg_kit_factory.dart | 214 ++ .../src/ffmpeg_kit_flutter_initializer.dart | 333 ++ .../lib/statistics.dart | 81 + .../lib/statistics_callback.dart | 23 + .../lib/stream_information.dart | 124 + .../pubspec.yaml | 26 + .../scripts/build_spm_artifacts.sh | 119 + .../scripts/setup_ios.sh | 201 ++ .../scripts/setup_macos.sh | 93 + .../flutter_inappwebview_android/CHANGELOG.md | 103 + .../flutter_inappwebview_android/JJ_PATCH.md | 10 + packages/flutter_inappwebview_android/LICENSE | 201 ++ .../flutter_inappwebview_android/README.md | 13 + .../analysis_options.yaml | 15 + .../android/build.gradle | 57 + .../android/gradle.properties | 3 + .../android/proguard-rules.pro | 19 + .../android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 43 + .../ISettings.java | 11 + .../InAppWebViewFileProvider.java | 9 + .../InAppWebViewFlutterPlugin.java | 224 ++ .../MyCookieManager.java | 437 +++ .../MyWebStorage.java | 138 + .../PlatformUtil.java | 73 + .../flutter_inappwebview_android/Util.java | 386 +++ .../WebViewFeatureManager.java | 47 + .../ActionBroadcastReceiver.java | 47 + .../ChromeCustomTabsActivity.java | 462 +++ ...hromeCustomTabsActivitySingleInstance.java | 7 + .../ChromeCustomTabsChannelDelegate.java | 251 ++ .../ChromeCustomTabsSettings.java | 187 ++ .../ChromeSafariBrowserManager.java | 139 + .../CustomTabActivityHelper.java | 177 ++ .../chrome_custom_tabs/CustomTabsHelper.java | 134 + .../chrome_custom_tabs/KeepAliveService.java | 18 + .../NoHistoryCustomTabsActivityCallbacks.java | 77 + .../chrome_custom_tabs/ServiceConnection.java | 32 + .../ServiceConnectionCallback.java | 19 + .../TrustedWebActivity.java | 84 + .../TrustedWebActivitySingleInstance.java | 7 + .../content_blocker/ContentBlocker.java | 59 + .../content_blocker/ContentBlockerAction.java | 71 + .../ContentBlockerActionType.java | 30 + .../ContentBlockerHandler.java | 336 ++ .../ContentBlockerTrigger.java | 185 ++ .../ContentBlockerTriggerResourceType.java | 36 + .../CredentialDatabase.java | 95 + .../CredentialDatabaseHandler.java | 157 + .../CredentialDatabaseHelper.java | 66 + .../URLCredentialContract.java | 15 + .../credential_database/URLCredentialDao.java | 106 + .../URLProtectionSpaceContract.java | 16 + .../URLProtectionSpaceDao.java | 100 + .../FindInteractionChannelDelegate.java | 94 + .../FindInteractionController.java | 73 + .../FindInteractionSettings.java | 45 + .../HeadlessInAppWebView.java | 159 + .../HeadlessInAppWebViewManager.java | 98 + .../HeadlessWebViewChannelDelegate.java | 71 + .../ActivityResultListener.java | 8 + .../in_app_browser/InAppBrowserActivity.java | 643 ++++ .../InAppBrowserChannelDelegate.java | 39 + .../in_app_browser/InAppBrowserDelegate.java | 16 + .../in_app_browser/InAppBrowserManager.java | 219 ++ .../in_app_browser/InAppBrowserSettings.java | 109 + .../InterceptAjaxRequestJS.java | 266 ++ .../InterceptFetchRequestJS.java | 152 + .../plugin_scripts_js/JavaScriptBridgeJS.java | 250 ++ .../plugin_scripts_js/OnLoadResourceJS.java | 35 + .../OnWindowBlurEventJS.java | 22 + .../OnWindowFocusEventJS.java | 22 + .../plugin_scripts_js/PluginScriptsUtil.java | 89 + .../plugin_scripts_js/PrintJS.java | 24 + .../plugin_scripts_js/PromisePolyfillJS.java | 21 + .../print_job/PrintJobChannelDelegate.java | 70 + .../print_job/PrintJobController.java | 96 + .../print_job/PrintJobManager.java | 59 + .../print_job/PrintJobSettings.java | 99 + .../ProcessGlobalConfigManager.java | 56 + .../ProcessGlobalConfigSettings.java | 119 + .../proxy/ProxyManager.java | 129 + .../proxy/ProxySettings.java | 98 + .../PullToRefreshChannelDelegate.java | 123 + .../pull_to_refresh/PullToRefreshLayout.java | 86 + .../PullToRefreshSettings.java | 80 + .../ServiceWorkerChannelDelegate.java | 147 + .../service_worker/ServiceWorkerManager.java | 115 + .../TracingControllerChannelDelegate.java | 78 + .../tracing/TracingControllerManager.java | 61 + .../tracing/TracingSettings.java | 61 + .../types/AndroidResource.java | 103 + .../types/BaseCallbackResultImpl.java | 47 + .../types/ChannelDelegateImpl.java | 37 + .../types/ClientCertChallenge.java | 85 + .../types/ClientCertResponse.java | 105 + .../types/ContentWorld.java | 63 + .../types/CreateWindowAction.java | 70 + .../types/CustomSchemeResponse.java | 89 + .../types/CustomTabsActionButton.java | 104 + .../types/CustomTabsMenuItem.java | 71 + .../types/CustomTabsSecondaryToolbar.java | 83 + .../types/Disposable.java | 5 + .../types/DownloadStartRequest.java | 150 + .../types/FindSession.java | 76 + ...olocationPermissionShowPromptResponse.java | 84 + .../types/HitTestResult.java | 79 + .../types/HttpAuthResponse.java | 102 + .../types/HttpAuthenticationChallenge.java | 71 + .../types/ICallbackResult.java | 13 + .../types/IChannelDelegate.java | 10 + .../types/InAppBrowserMenuItem.java | 146 + .../types/JsAlertResponse.java | 98 + .../types/JsBeforeUnloadResponse.java | 113 + .../types/JsConfirmResponse.java | 113 + .../types/JsPromptResponse.java | 145 + .../types/MarginsExt.java | 150 + .../types/MediaSizeExt.java | 134 + .../types/NavigationAction.java | 95 + .../types/NavigationActionPolicy.java | 33 + .../types/PermissionResponse.java | 73 + .../types/PluginScript.java | 48 + .../types/PreferredContentModeOptionType.java | 29 + .../types/PrintAttributesExt.java | 56 + .../types/PrintJobInfoExt.java | 56 + .../types/ProxyRuleExt.java | 83 + .../types/ResolutionExt.java | 134 + .../types/SafeBrowsingResponse.java | 69 + .../types/ServerTrustAuthResponse.java | 54 + .../types/ServerTrustChallenge.java | 12 + .../types/Size2D.java | 81 + .../types/SslCertificateExt.java | 77 + .../types/SslErrorExt.java | 56 + .../types/SyncBaseCallbackResultImpl.java | 47 + .../types/URLAuthenticationChallenge.java | 48 + .../types/URLCredential.java | 101 + .../types/URLProtectionSpace.java | 155 + .../types/URLRequest.java | 128 + .../types/UserContentController.java | 473 +++ .../types/UserScript.java | 129 + .../types/UserScriptInjectionTime.java | 28 + .../types/WebMessage.java | 23 + .../types/WebMessageCompatExt.java | 117 + .../types/WebMessagePort.java | 136 + .../types/WebMessagePortCompatExt.java | 78 + .../types/WebResourceErrorExt.java | 91 + .../types/WebResourceRequestExt.java | 146 + .../types/WebResourceResponseExt.java | 177 ++ .../types/WebViewAssetLoaderExt.java | 209 ++ .../webview/ContextMenuSettings.java | 49 + .../webview/FlutterWebViewFactory.java | 81 + .../webview/InAppWebViewInterface.java | 119 + .../webview/InAppWebViewManager.java | 219 ++ .../webview/JavaScriptBridgeInterface.java | 173 + .../webview/PlatformWebView.java | 9 + .../webview/WebViewChannelDelegate.java | 1318 ++++++++ .../WebViewChannelDelegateMethods.java | 86 + .../in_app_webview/DisplayListenerProxy.java | 122 + .../in_app_webview/FlutterWebView.java | 189 ++ .../webview/in_app_webview/InAppWebView.java | 2101 +++++++++++++ .../InAppWebViewChromeClient.java | 1330 ++++++++ .../in_app_webview/InAppWebViewClient.java | 813 +++++ .../InAppWebViewClientCompat.java | 840 +++++ .../InAppWebViewRenderProcessClient.java | 99 + .../in_app_webview/InAppWebViewSettings.java | 647 ++++ .../in_app_webview/InputAwareWebView.java | 273 ++ ...readedInputConnectionProxyAdapterView.java | 99 + .../web_message/WebMessageChannel.java | 166 + .../WebMessageChannelChannelDelegate.java | 72 + .../web_message/WebMessageListener.java | 241 ++ .../WebMessageListenerChannelDelegate.java | 56 + .../drawable/floating_action_mode_shape.xml | 6 + .../src/main/res/layout/activity_web_view.xml | 32 + .../res/layout/chrome_custom_tabs_layout.xml | 6 + .../main/res/layout/floating_action_mode.xml | 16 + .../res/layout/floating_action_mode_item.xml | 12 + .../android/src/main/res/menu/menu_main.xml | 49 + .../android/src/main/res/values/strings.xml | 9 + .../android/src/main/res/values/styles.xml | 19 + .../src/main/res/xml/provider_paths.xml | 4 + .../flutter_inappwebview_android/build.yaml | 5 + .../lib/flutter_inappwebview_android.dart | 3 + .../chrome_safari_browser.dart | 376 +++ .../lib/src/chrome_safari_browser/main.dart | 1 + .../lib/src/cookie_manager.dart | 220 ++ .../find_interaction_controller.dart | 125 + .../lib/src/find_interaction/main.dart | 2 + .../src/http_auth_credentials_database.dart | 155 + .../src/in_app_browser/in_app_browser.dart | 375 +++ .../lib/src/in_app_browser/main.dart | 1 + .../src/in_app_webview/_static_channel.dart | 4 + .../headless_in_app_webview.dart | 440 +++ .../src/in_app_webview/in_app_webview.dart | 475 +++ .../in_app_webview_controller.dart | 2779 +++++++++++++++++ .../lib/src/in_app_webview/main.dart | 3 + .../lib/src/inappwebview_platform.dart | 376 +++ .../lib/src/main.dart | 18 + .../lib/src/platform_util.dart | 64 + .../lib/src/print_job/main.dart | 1 + .../src/print_job/print_job_controller.dart | 77 + .../lib/src/process_global_config.dart | 76 + .../lib/src/proxy_controller.dart | 81 + .../lib/src/pull_to_refresh/main.dart | 1 + .../pull_to_refresh_controller.dart | 163 + .../lib/src/service_worker_controller.dart | 161 + .../lib/src/tracing_controller.dart | 88 + .../lib/src/web_message/main.dart | 3 + .../src/web_message/web_message_channel.dart | 120 + .../src/web_message/web_message_listener.dart | 164 + .../lib/src/web_message/web_message_port.dart | 95 + .../lib/src/web_storage/main.dart | 2 + .../lib/src/web_storage/web_storage.dart | 257 ++ .../src/web_storage/web_storage_manager.dart | 116 + .../lib/src/webview_asset_loader.dart | 237 ++ .../lib/src/webview_feature.dart | 89 + .../flutter_inappwebview_android/pubspec.yaml | 83 + .../flutter_inappwebview_android_test.dart | 0 packages/whisper_ggml/JJ_PATCH.md | 5 + packages/whisper_ggml/android/build.gradle | 2 +- .../lib/src/whisper_audio_convert.dart | 6 +- packages/whisper_ggml/pubspec.yaml | 2 +- pubspec.lock | 22 +- pubspec.yaml | 9 + 282 files changed, 37934 insertions(+), 36 deletions(-) create mode 100644 lib/platform/android/android_shell.dart create mode 100644 lib/platform/android/android_storage.dart create mode 100644 lib/platform/android/ffmpeg_kit_media_tool.dart create mode 100644 lib/ui/android_file_browser.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/CHANGELOG.md create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/JJ_PATCH.md create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/LICENSE create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/README.md create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/analysis_options.yaml create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/build.gradle create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/consumer-rules.pro create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle.properties create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/gradlew create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/gradlew.bat create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/proguard-rules.pro create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/settings.gradle create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/AndroidManifest.xml create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegKitFlutterMethodResultHandler.java create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegKitFlutterPlugin.java create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegSessionExecuteTask.java create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFprobeSessionExecuteTask.java create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/MediaInformationSessionExecuteTask.java create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/WriteToPipeTask.java create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/abstract_session.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/arch_detect.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/chapter.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/extensions.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_kit.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_kit_config.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_session.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_session_complete_callback.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_kit.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_session.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_session_complete_callback.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/level.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/log.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/log_callback.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/log_redirection_strategy.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_json_parser.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_session.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_session_complete_callback.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/packages.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/return_code.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/session.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/session_state.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/signal.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/src/ffmpeg_kit_factory.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/src/ffmpeg_kit_flutter_initializer.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/statistics.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/statistics_callback.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/lib/stream_information.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/pubspec.yaml create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/scripts/build_spm_artifacts.sh create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/scripts/setup_ios.sh create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/scripts/setup_macos.sh create mode 100644 packages/flutter_inappwebview_android/CHANGELOG.md create mode 100644 packages/flutter_inappwebview_android/JJ_PATCH.md create mode 100644 packages/flutter_inappwebview_android/LICENSE create mode 100644 packages/flutter_inappwebview_android/README.md create mode 100644 packages/flutter_inappwebview_android/analysis_options.yaml create mode 100644 packages/flutter_inappwebview_android/android/build.gradle create mode 100644 packages/flutter_inappwebview_android/android/gradle.properties create mode 100644 packages/flutter_inappwebview_android/android/proguard-rules.pro create mode 100644 packages/flutter_inappwebview_android/android/settings.gradle create mode 100644 packages/flutter_inappwebview_android/android/src/main/AndroidManifest.xml create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/ISettings.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFileProvider.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFlutterPlugin.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyCookieManager.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyWebStorage.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/PlatformUtil.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/Util.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/WebViewFeatureManager.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ActionBroadcastReceiver.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivity.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivitySingleInstance.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsSettings.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeSafariBrowserManager.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabActivityHelper.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabsHelper.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/KeepAliveService.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/NoHistoryCustomTabsActivityCallbacks.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnection.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnectionCallback.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivity.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivitySingleInstance.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlocker.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerAction.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerActionType.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerHandler.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTrigger.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTriggerResourceType.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabase.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHandler.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHelper.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialContract.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialDao.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceContract.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceDao.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionController.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionSettings.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebView.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebViewManager.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessWebViewChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/ActivityResultListener.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserActivity.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserManager.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserSettings.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptAjaxRequestJS.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptFetchRequestJS.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/JavaScriptBridgeJS.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnLoadResourceJS.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowBlurEventJS.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowFocusEventJS.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PluginScriptsUtil.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PrintJS.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PromisePolyfillJS.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/print_job/PrintJobChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/print_job/PrintJobController.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/print_job/PrintJobManager.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/print_job/PrintJobSettings.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigManager.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigSettings.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxyManager.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxySettings.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshLayout.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshSettings.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerManager.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerManager.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingSettings.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/AndroidResource.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/BaseCallbackResultImpl.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ChannelDelegateImpl.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertChallenge.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertResponse.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ContentWorld.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CreateWindowAction.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomSchemeResponse.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsActionButton.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsMenuItem.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsSecondaryToolbar.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Disposable.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/DownloadStartRequest.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/FindSession.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/GeolocationPermissionShowPromptResponse.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HitTestResult.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthResponse.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthenticationChallenge.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ICallbackResult.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/IChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/InAppBrowserMenuItem.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsAlertResponse.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsBeforeUnloadResponse.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsConfirmResponse.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsPromptResponse.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MarginsExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MediaSizeExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationAction.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationActionPolicy.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PermissionResponse.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PluginScript.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PreferredContentModeOptionType.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintAttributesExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintJobInfoExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ProxyRuleExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ResolutionExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SafeBrowsingResponse.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustAuthResponse.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustChallenge.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Size2D.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslCertificateExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslErrorExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SyncBaseCallbackResultImpl.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLAuthenticationChallenge.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLCredential.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLProtectionSpace.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLRequest.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserContentController.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScript.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScriptInjectionTime.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessage.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessageCompatExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePort.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePortCompatExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceErrorExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceRequestExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceResponseExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebViewAssetLoaderExt.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/ContextMenuSettings.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/FlutterWebViewFactory.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewInterface.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewManager.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/JavaScriptBridgeInterface.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/PlatformWebView.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegateMethods.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/DisplayListenerProxy.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/FlutterWebView.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebView.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewChromeClient.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClient.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClientCompat.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewRenderProcessClient.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewSettings.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InputAwareWebView.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/ThreadedInputConnectionProxyAdapterView.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannel.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannelChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListener.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListenerChannelDelegate.java create mode 100644 packages/flutter_inappwebview_android/android/src/main/res/drawable/floating_action_mode_shape.xml create mode 100644 packages/flutter_inappwebview_android/android/src/main/res/layout/activity_web_view.xml create mode 100644 packages/flutter_inappwebview_android/android/src/main/res/layout/chrome_custom_tabs_layout.xml create mode 100644 packages/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode.xml create mode 100644 packages/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode_item.xml create mode 100644 packages/flutter_inappwebview_android/android/src/main/res/menu/menu_main.xml create mode 100644 packages/flutter_inappwebview_android/android/src/main/res/values/strings.xml create mode 100644 packages/flutter_inappwebview_android/android/src/main/res/values/styles.xml create mode 100644 packages/flutter_inappwebview_android/android/src/main/res/xml/provider_paths.xml create mode 100644 packages/flutter_inappwebview_android/build.yaml create mode 100644 packages/flutter_inappwebview_android/lib/flutter_inappwebview_android.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/chrome_safari_browser/chrome_safari_browser.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/chrome_safari_browser/main.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/cookie_manager.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/find_interaction/find_interaction_controller.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/find_interaction/main.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/http_auth_credentials_database.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/in_app_browser/in_app_browser.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/in_app_browser/main.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/in_app_webview/_static_channel.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/in_app_webview/headless_in_app_webview.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview_controller.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/in_app_webview/main.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/inappwebview_platform.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/main.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/platform_util.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/print_job/main.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/print_job/print_job_controller.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/process_global_config.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/proxy_controller.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/pull_to_refresh/main.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/pull_to_refresh/pull_to_refresh_controller.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/service_worker_controller.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/tracing_controller.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/web_message/main.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/web_message/web_message_channel.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/web_message/web_message_listener.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/web_message/web_message_port.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/web_storage/main.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/web_storage/web_storage.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/web_storage/web_storage_manager.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/webview_asset_loader.dart create mode 100644 packages/flutter_inappwebview_android/lib/src/webview_feature.dart create mode 100644 packages/flutter_inappwebview_android/pubspec.yaml create mode 100644 packages/flutter_inappwebview_android/test/flutter_inappwebview_android_test.dart diff --git a/.gitignore b/.gitignore index 78c6a5a..b777be1 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,8 @@ app.*.map.json # 내장 Chrome: 빌드할 때 내려받는 CEF 배포본 (약 330MB) /packages/webview_cef/third/cef/ + +# Android 빌드 중간 파일 +/android/build/ +/packages/*/android/.cxx/ +/packages/*/android/build/ diff --git a/analysis_options.yaml b/analysis_options.yaml index 1477853..eb69790 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -14,7 +14,9 @@ analyzer: - build/** - android/** - windows/** - - packages/webview_cef/** # 고친 외부 부품 (원본 코드 그대로) + - packages/webview_cef/** + - packages/flutter_inappwebview_android/** + - packages/ffmpeg_kit_flutter_new_min_gpl/** # 고친 외부 부품 (원본 코드 그대로) linter: # The lint rules applied to this project can be customized in the diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 7cd07ac..66a499d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,13 +1,22 @@ +import java.util.Properties + plugins { id("com.android.application") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } +// 배포 서명 키 (android/key.properties, 저장소에 올리지 않음). 없으면 디버그 키로 서명. +val keyProps = Properties().apply { + val f = rootProject.file("key.properties") + if (f.exists()) f.inputStream().use { load(it) } +} + android { namespace = "com.jj.jj_mkvmaker" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + // whisper_ggml 이 NDK 29 를 요구 (더 낮은 버전용 코드와 호환) + ndkVersion = "29.0.13113456" compileOptions { sourceCompatibility = JavaVersion.VERSION_17 @@ -15,25 +24,41 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.jj.jj_mkvmaker" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion + // ffmpeg-kit 은 Android 7.0 (API 24) 이상 + minSdk = maxOf(24, flutter.minSdkVersion) targetSdk = flutter.targetSdkVersion - // Uses the version code from pubspec.yaml. When using split APKs, 1000 * ABI_VERSION - // is added automatically by Flutter. (https://developer.android.com/studio/build/configure-apk-splits#configure-APK-versions) - // You can force using the value of versionCode by specifying the `-P force-version-code-ignoring-abi=true` - // flag during build. versionCode = flutter.versionCode versionName = flutter.versionName + // 요즘 휴대폰 · 태블릿 (64비트 ARM) 만. 플러그인이 다른 CPU 용 라이브러리를 넣어도 앱 본체가 없으면 실행되지 않으므로 뺀다 + ndk { + abiFilters += listOf("arm64-v8a") + } + } + + signingConfigs { + if (keyProps.getProperty("storeFile") != null) { + create("release") { + storeFile = file(keyProps.getProperty("storeFile")) + storePassword = keyProps.getProperty("storePassword") + keyAlias = keyProps.getProperty("keyAlias") + keyPassword = keyProps.getProperty("keyPassword") + } + } } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") + signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug") + } + } + + // ffmpeg · whisper 등 네이티브 라이브러리를 압축하지 않고 그대로 (설치 후 바로 로드) + packaging { + jniLibs { + useLegacyPackaging = true + // 64비트 ARM 만 (위 abiFilters 와 같은 뜻, 플러그인 라이브러리까지 확실히 빼기) + excludes += setOf("lib/x86_64/**", "lib/x86/**", "lib/armeabi-v7a/**") } } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9e7d331..24f9306 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,16 @@ + + + + + + + + android:icon="@mipmap/ic_launcher" + android:requestLegacyExternalStorage="true"> + when (call.method) { + "hasAllFilesAccess" -> result.success(hasAllFilesAccess()) + "requestAllFilesAccess" -> { + requestAllFilesAccess() + result.success(null) + } + "storageRoot" -> result.success(Environment.getExternalStorageDirectory().path) + else -> result.notImplemented() + } + } + } + + private fun hasAllFilesAccess(): Boolean = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Environment.isExternalStorageManager() + } else { + checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED + } + + private fun requestAllFilesAccess() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + startActivity(Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:$packageName"))) + } catch (e: Exception) { + startActivity(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)) + } + } else { + requestPermissions( + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), 1 + ) + } + } +} diff --git a/android/build.gradle.kts b/android/build.gradle.kts index dbee657..0473575 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -15,6 +15,12 @@ subprojects { val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) project.layout.buildDirectory.value(newSubprojectBuildDir) } +// 오래된 플러그인 (onnxruntime 등) 은 낮은 compileSdk 로 되어 있어 새 라이브러리와 맞지 않는다 → 모두 36 으로 +subprojects { + afterEvaluate { + (extensions.findByName("android") as? com.android.build.gradle.BaseExtension)?.compileSdkVersion(36) + } +} subprojects { project.evaluationDependsOn(":app") } diff --git a/android/gradle.properties b/android/gradle.properties index e96108c..8eded1d 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,7 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +# 이 PC 는 메모리가 빠듯해서 작게 (원래 -Xmx8G) +org.gradle.jvmargs=-Xmx3G -XX:MaxMetaspaceSize=1G -XX:ReservedCodeCacheSize=256m +org.gradle.workers.max=2 +kotlin.daemon.jvmargs=-Xmx1G android.useAndroidX=true # This newDsl flag was added by the Flutter template android.newDsl=false diff --git a/lib/main.dart b/lib/main.dart index 9d438cd..c2f1802 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; @@ -13,6 +14,7 @@ import 'app/download_manager.dart'; import 'app/settings.dart'; import 'core/app_update.dart'; import 'core/playlist.dart'; +import 'platform/android/android_storage.dart'; import 'platform/windows/app_paths.dart'; import 'platform/windows/cef_runtime.dart'; import 'platform/windows/com_guard.dart'; @@ -49,6 +51,7 @@ Future _prepare() async { Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); final dataDir = await _prepare(); + if (Platform.isAndroid) return runAndroid(dataDir); // 이미 실행 중이면 인수(탐색기에서 고른 파일)를 넘기고 끝낸다 final request = LaunchRequest.parse(args); @@ -310,6 +313,65 @@ String lastWorkText(AppController c) { return c.logs.last.replaceFirst(RegExp(r'^\[[\d:]+\]\s*'), '').split('\n').first; } +/// Android: 창 하나. 트레이 · 단축키 · 탐색기 연결 · 다운로드 (yt-dlp · aria2) · 업데이트는 없다. +/// 데스크톱 화면을 그대로 쓰므로 가로 화면으로 띄운다. +Future runAndroid(String dataDir) async { + await SystemChrome.setPreferredOrientations( + [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]); + final services = PlatformServices.create(); + final logs = Directory(p.join(dataDir, 'Logs'))..createSync(recursive: true); + final controller = AppController(services, settingsStore: SettingsStore())..logFile = p.join(logs.path, 'app.log'); + controller.note('── 시작 $appTitle (Android) ──'); + await controller.init(); + final bookmarks = BookmarksController(); + await bookmarks.load(); + final navigatorKey = GlobalKey(); + final messengerKey = GlobalKey(); + AndroidStorageService.navigatorKey = navigatorKey; + + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (controller.settings.startScreen == 'browser') { + navigatorKey.currentState + ?.push(MaterialPageRoute(builder: (_) => BrowserPage(c: controller, bookmarks: bookmarks))); + } + // 동영상을 읽고 옆에 MKV 를 만들려면 저장소 전체 접근이 필요하다 + if (!await AndroidAccess.hasAllFiles()) { + messengerKey.currentState?.showSnackBar(SnackBar( + duration: const Duration(seconds: 20), + content: const Text('동영상을 고르고 MKV 를 만들려면 "모든 파일에 대한 접근" 권한이 필요합니다.'), + action: SnackBarAction(label: '허용', onPressed: AndroidAccess.request), + )); + } + }); + + runApp(JjCapCutApp( + controller: controller, + bookmarks: bookmarks, + navigatorKey: navigatorKey, + messengerKey: messengerKey, + onExit: () async { + final ctx = navigatorKey.currentContext; + if (controller.busy && ctx != null && ctx.mounted) { + final ok = await showDialog( + context: ctx, + builder: (c) => AlertDialog( + title: const Text('종료'), + content: Text('진행 중인 작업이 있습니다: ${controller.currentJob ?? ''}\n중지하고 끝낼까요?'), + actions: [ + TextButton(onPressed: () => Navigator.pop(c, false), child: const Text('취소')), + FilledButton(onPressed: () => Navigator.pop(c, true), child: const Text('종료')), + ], + ), + ); + if (ok != true) return; + } + controller.cancel(); + controller.note('── 정상 종료 ──'); + await services.shell.quit(); + }, + )); +} + /// 탐색기에서 연 동영상의 새 창: 바로 재생하고, ← 로 나오면 같은 동영상 목록의 MKV 화면. /// 트레이 · 단축키 · 다운로드 · 브라우저는 원래 창이 맡는다. 닫으면 이 창만 끝난다. /// 동영상 목록은 원래 창과 같은 파일을 보므로 어느 창에서 넣거나 지워도 서로 맞춰진다. diff --git a/lib/platform/android/android_shell.dart b/lib/platform/android/android_shell.dart new file mode 100644 index 0000000..38ec167 --- /dev/null +++ b/lib/platform/android/android_shell.dart @@ -0,0 +1,20 @@ +import 'package:flutter/services.dart'; + +import '../../services/app_shell.dart'; + +/// Android: 창 · 트레이 · 단축키 · 탐색기 연결은 없음. 종료와 전체 화면만. +class AndroidShell extends NoopShell { + bool _full = false; + + @override + Future quit() => SystemNavigator.pop(); + + @override + Future setFullScreen(bool on) async { + _full = on; + await SystemChrome.setEnabledSystemUIMode(on ? SystemUiMode.immersiveSticky : SystemUiMode.edgeToEdge); + } + + @override + Future isFullScreen() async => _full; +} diff --git a/lib/platform/android/android_storage.dart b/lib/platform/android/android_storage.dart new file mode 100644 index 0000000..89bfe9a --- /dev/null +++ b/lib/platform/android/android_storage.dart @@ -0,0 +1,67 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +import '../../core/subtitle_detector.dart'; +import '../../ui/android_file_browser.dart'; +import '../windows/desktop_storage_service.dart'; + +/// Android 저장소 전체 접근 (MainActivity.kt 의 "jj_mkvmaker/android") +class AndroidAccess { + static const _ch = MethodChannel('jj_mkvmaker/android'); + + static Future hasAllFiles() async { + try { + return await _ch.invokeMethod('hasAllFilesAccess') ?? false; + } catch (_) { + return false; + } + } + + /// 설정 화면 (모든 파일 접근 허용) 을 연다 + static Future request() async { + try { + await _ch.invokeMethod('requestAllFilesAccess'); + } catch (_) {} + } + + /// 내장 저장소 맨 위 (예: /storage/emulated/0) + static Future storageRoot() async { + try { + return await _ch.invokeMethod('storageRoot') ?? '/storage/emulated/0'; + } catch (_) { + return '/storage/emulated/0'; + } + } +} + +/// Android: 실제 파일 경로가 필요하다 (MKV 를 동영상 옆 jj_mkv 폴더에 만든다). +/// 시스템 파일 선택기는 사본을 앱 캐시에 만들어 큰 동영상에 맞지 않으므로 앱 안 파일 고르기 화면을 쓴다. +class AndroidStorageService extends DesktopStorageService { + /// 파일 고르기 화면을 띄울 곳 (main 에서 정함) + static GlobalKey? navigatorKey; + + Future> _pick(String title, List extensions, {String? initialDirectory}) async { + final ctx = navigatorKey?.currentContext; + if (ctx == null) return []; + return await showAndroidFileBrowser(ctx, title: title, extensions: extensions, initialDirectory: initialDirectory) ?? + const []; + } + + @override + Future> pickVideos() => _pick('동영상 선택', videoExtensions); + + @override + Future> pickSubtitles({String? initialDirectory}) => + _pick('자막 파일 선택', subtitleExtensions, initialDirectory: initialDirectory); + + @override + Future tempDirectory() async { + final dir = Directory(p.join((await getTemporaryDirectory()).path, 'jj_mkvmaker')); + await dir.create(recursive: true); + return dir.path; + } +} diff --git a/lib/platform/android/ffmpeg_kit_media_tool.dart b/lib/platform/android/ffmpeg_kit_media_tool.dart new file mode 100644 index 0000000..3ecc5ec --- /dev/null +++ b/lib/platform/android/ffmpeg_kit_media_tool.dart @@ -0,0 +1,114 @@ +import 'dart:async'; + +import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit.dart'; +import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit_config.dart'; +import 'package:ffmpeg_kit_flutter_new_min_gpl/ffprobe_kit.dart'; +import 'package:ffmpeg_kit_flutter_new_min_gpl/return_code.dart'; + +import '../../core/models.dart'; +import '../../services/media_tool.dart'; +import '../windows/process_media_tool.dart' show parseEncoderList, parseFfmpegInfo, parseProbeJson; + +/// Android: 앱에 넣은 FFmpeg 라이브러리 (ffmpeg-kit) 로 실행하는 구현. +/// 명령 인수는 데스크톱 (ffmpeg.exe) 과 같다. 진행률만 "-progress pipe:1" 대신 통계 콜백으로 받는다. +class FfmpegKitMediaTool implements MediaTool { + /// 실행 중인 작업 (취소용) + final Set _running = {}; + final Set _cancelled = {}; + + @override + Future version() async { + try { + final v = await FFmpegKitConfig.getFFmpegVersion(); + return v == null ? null : 'ffmpeg version $v (Android)'; + } catch (_) { + return null; + } + } + + @override + Future> encoders() async { + try { + final s = await FFmpegKit.executeWithArguments(['-hide_banner', '-encoders']); + return parseEncoderList(await s.getOutput() ?? ''); + } catch (_) { + return {}; + } + } + + @override + Future probe(String path) async { + try { + final s = await FFprobeKit.executeWithArguments( + ['-v', 'error', '-print_format', 'json', '-show_format', '-show_streams', path]); + if (ReturnCode.isSuccess(await s.getReturnCode())) { + final out = await s.getOutput() ?? ''; + final start = out.indexOf('{'); + if (start >= 0) return parseProbeJson(out.substring(start)); + } + } catch (_) {} + // ffprobe 로 안 되면 ffmpeg -i 의 출력으로 + final s = await FFmpegKit.executeWithArguments(['-hide_banner', '-nostdin', '-i', path]); + final text = await s.getOutput() ?? ''; + final info = parseFfmpegInfo(text); + if (info.streams.isEmpty) { + throw MediaToolException('파일을 분석할 수 없습니다: ${text.trim().split('\n').last}'); + } + return info; + } + + @override + Future runFfmpeg(List args, {Duration? duration, ProgressCallback? onProgress}) async { + // 표준 출력으로 진행률을 쓰는 인수는 빼고 통계 콜백을 쓴다 + final a = []; + for (var i = 0; i < args.length; i++) { + if (args[i] == '-progress' && i + 1 < args.length) { + i++; + continue; + } + a.add(args[i]); + } + final totalMs = duration?.inMilliseconds ?? 0; + final done = Completer(); + final errTail = []; + final session = await FFmpegKit.executeWithArgumentsAsync( + a, + (s) async { + final id = s.getSessionId(); + _running.remove(id); + final rc = await s.getReturnCode(); + if (_cancelled.remove(id) || ReturnCode.isCancel(rc)) { + done.completeError(const MediaToolException('사용자가 취소했습니다.')); + } else if (!ReturnCode.isSuccess(rc)) { + done.completeError(MediaToolException('ffmpeg 오류 (코드 ${rc?.getValue()})\n${errTail.join('\n')}')); + } else { + done.complete(); + } + }, + (log) { + for (final line in log.getMessage().split('\n')) { + if (line.trim().isEmpty) continue; + errTail.add(line); + if (errTail.length > 15) errTail.removeAt(0); + } + }, + (st) { + if (onProgress != null && totalMs > 0 && st.getTime() > 0) { + onProgress((st.getTime() / totalMs).clamp(0.0, 1.0)); + } + }, + ); + final id = session.getSessionId(); + if (id != null && !done.isCompleted) _running.add(id); + await done.future; + onProgress?.call(1.0); + } + + @override + void cancel() { + for (final id in _running.toList()) { + _cancelled.add(id); + unawaited(FFmpegKit.cancel(id)); + } + } +} diff --git a/lib/services/platform_services.dart b/lib/services/platform_services.dart index 0f93411..c8cbe0e 100644 --- a/lib/services/platform_services.dart +++ b/lib/services/platform_services.dart @@ -9,6 +9,9 @@ import '../core/download_detect.dart'; import '../platform/common/nllb_translator.dart'; import '../platform/common/opensubtitles_provider.dart'; import '../platform/common/whisper_recognizer.dart'; +import '../platform/android/android_shell.dart'; +import '../platform/android/android_storage.dart'; +import '../platform/android/ffmpeg_kit_media_tool.dart'; import '../platform/windows/aria2_backend.dart'; import '../platform/windows/desktop_shell.dart'; import '../platform/windows/desktop_storage_service.dart'; @@ -115,6 +118,26 @@ class PlatformServices { ], ); } + if (Platform.isAndroid) { + // 다운로드 (yt-dlp · aria2) · 업데이트 · 탐색기 연결은 데스크톱 전용 + MediaKitPreviewPlayer.ensureInitialized(); + return PlatformServices( + mediaTool: FfmpegKitMediaTool(), + storage: AndroidStorageService(), + createPlayer: MediaKitPreviewPlayer.new, + createMediaPlayer: MediaKitFullPlayer.new, + createRecognizer: WhisperRecognizer.new, + createTranslator: NllbTranslator.new, + shell: AndroidShell(), + createSubtitleProviders: (s) => [ + OpenSubtitlesProvider( + apiKey: () => s().openSubtitlesKey, + username: () => s().openSubtitlesUser, + password: () => s().openSubtitlesPassword, + ), + ], + ); + } throw UnsupportedError('아직 지원하지 않는 플랫폼입니다: ${Platform.operatingSystem}'); } } diff --git a/lib/ui/android_file_browser.dart b/lib/ui/android_file_browser.dart new file mode 100644 index 0000000..7a74116 --- /dev/null +++ b/lib/ui/android_file_browser.dart @@ -0,0 +1,207 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as p; + +import '../platform/android/android_storage.dart'; +import 'theme.dart'; + +/// 마지막으로 본 폴더 (다음에 열 때 여기서) +String? _lastDir; + +/// Android 앱 안 파일 고르기: 폴더를 오가며 [extensions] 파일을 여러 개 고른다. 고른 파일의 실제 경로를 돌려준다. +Future?> showAndroidFileBrowser( + BuildContext context, { + required String title, + required List extensions, + String? initialDirectory, +}) => + Navigator.of(context).push>(MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => _FileBrowser(title: title, extensions: extensions, initialDirectory: initialDirectory), + )); + +class _FileBrowser extends StatefulWidget { + final String title; + final List extensions; + final String? initialDirectory; + const _FileBrowser({required this.title, required this.extensions, this.initialDirectory}); + + @override + State<_FileBrowser> createState() => _FileBrowserState(); +} + +class _FileBrowserState extends State<_FileBrowser> { + String _root = '/storage/emulated/0'; + String? _dir; + bool? _allowed; + List _dirs = []; + List _files = []; + final _picked = {}; + + @override + void initState() { + super.initState(); + _start(); + } + + Future _start() async { + _root = await AndroidAccess.storageRoot(); + final allowed = await AndroidAccess.hasAllFiles(); + final first = [widget.initialDirectory, _lastDir, _root].firstWhere( + (d) => d != null && Directory(d).existsSync(), + orElse: () => _root)!; + setState(() => _allowed = allowed); + if (allowed) _open(first); + } + + bool _match(String path) => widget.extensions.contains(p.extension(path).replaceFirst('.', '').toLowerCase()); + + void _open(String dir) { + final dirs = []; + final files = []; + try { + for (final e in Directory(dir).listSync(followLinks: false)) { + final name = p.basename(e.path); + if (name.startsWith('.')) continue; + if (e is Directory) dirs.add(e); + if (e is File && _match(e.path)) files.add(e); + } + } catch (_) {} + int byName(FileSystemEntity a, FileSystemEntity b) => + p.basename(a.path).toLowerCase().compareTo(p.basename(b.path).toLowerCase()); + dirs.sort(byName); + files.sort(byName); + setState(() { + _dir = _lastDir = dir; + _dirs = dirs; + _files = files; + }); + } + + static String _size(int b) { + if (b >= 1 << 30) return '${(b / (1 << 30)).toStringAsFixed(1)}GB'; + if (b >= 1 << 20) return '${(b / (1 << 20)).toStringAsFixed(1)}MB'; + return '${(b / 1024).ceil()}KB'; + } + + @override + Widget build(BuildContext context) { + final dir = _dir; + final atRoot = dir == null || p.equals(dir, _root) || !p.isWithin(_root, dir); + return PopScope( + canPop: atRoot, + onPopInvokedWithResult: (didPop, _) { + if (!didPop && dir != null) _open(p.dirname(dir)); + }, + child: Scaffold( + appBar: AppBar( + title: Text(widget.title), + actions: [ + if (_files.isNotEmpty) + TextButton( + onPressed: () => setState(() { + final all = _files.every((f) => _picked.contains(f.path)); + for (final f in _files) { + all ? _picked.remove(f.path) : _picked.add(f.path); + } + }), + child: const Text('이 폴더 전체'), + ), + Padding( + padding: const EdgeInsets.only(right: 8), + child: FilledButton( + onPressed: _picked.isEmpty ? null : () => Navigator.pop(context, _picked.toList()), + child: Text('추가 (${_picked.length})'), + ), + ), + ], + ), + body: switch (_allowed) { + null => const Center(child: CircularProgressIndicator()), + false => _permission(), + true => Column(children: [ + // 바로 가기 · 지금 폴더 + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.fromLTRB(8, 6, 8, 2), + child: Row(children: [ + for (final (label, sub) in [ + ('내장 저장소', ''), + ('Download', 'Download'), + ('Movies', 'Movies'), + ('DCIM', 'DCIM'), + ('jj_mkv', 'Movies/jj_mkv'), + ]) + if (Directory(p.join(_root, sub)).existsSync()) + Padding( + padding: const EdgeInsets.only(right: 6), + child: ActionChip(label: Text(label), onPressed: () => _open(p.join(_root, sub))), + ), + ]), + ), + ListTile( + dense: true, + leading: IconButton( + tooltip: '위 폴더', + icon: const Icon(Icons.arrow_upward), + onPressed: atRoot ? null : () => _open(p.dirname(dir)), + ), + title: Text( + dir == null ? '' : (p.equals(dir, _root) ? '내장 저장소' : p.relative(dir, from: _root)), + style: const TextStyle(color: JjColors.textDim), + ), + ), + const Divider(height: 1), + Expanded( + child: _dirs.isEmpty && _files.isEmpty + ? const Center(child: Text('이 폴더에는 고를 파일이 없습니다', style: TextStyle(color: JjColors.textDim))) + : ListView(children: [ + for (final d in _dirs) + ListTile( + leading: const Icon(Icons.folder, color: Colors.amber), + title: Text(p.basename(d.path)), + onTap: () => _open(d.path), + ), + for (final f in _files) + CheckboxListTile( + value: _picked.contains(f.path), + onChanged: (v) => setState(() => v! ? _picked.add(f.path) : _picked.remove(f.path)), + secondary: const Icon(Icons.movie_outlined), + title: Text(p.basename(f.path)), + subtitle: Text(_sizeOf(f)), + ), + ]), + ), + ]), + }, + ), + ); + } + + String _sizeOf(File f) { + try { + return _size(f.lengthSync()); + } catch (_) { + return ''; + } + } + + Widget _permission() => Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.folder_off_outlined, size: 48), + const SizedBox(height: 12), + const Text( + '동영상을 고르고, 동영상 옆 jj_mkv 폴더에 MKV 를 만들려면\n"모든 파일에 대한 접근" 권한이 필요합니다.', + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + FilledButton(onPressed: AndroidAccess.request, child: const Text('권한 허용 화면 열기')), + const SizedBox(height: 8), + TextButton(onPressed: _start, child: const Text('허용했으면 다시 확인')), + ]), + ), + ); +} diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index ee0f439..fb02adf 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -240,6 +240,8 @@ class _SettingsPageState extends State { listenable: c, builder: (context, _) { final s = c.settings; + // Android: 창 · 트레이 · 탐색기 · 다운로드 (yt-dlp · aria2) · 내장 Chrome 설정은 없음 + final desk = !Platform.isAndroid; return Scaffold( body: Column(children: [ Container( @@ -298,6 +300,7 @@ class _SettingsPageState extends State { ), _textTile('홈 주소', s.homeUrl, '웹 브라우저의 시작 · 🏠 주소', (v) => c.updateSettings((x) => x.homeUrl = v.trim().isEmpty ? 'https://www.youtube.com/' : v.trim())), + if (desk) ...[ ListTile( title: const Text('브라우저 엔진 (앱 안)'), subtitle: Text( @@ -356,6 +359,7 @@ class _SettingsPageState extends State { onChanged: (v) => c.updateSettings((x) => x.externalBrowser = v!), ), ), + ], _section('저장 위치'), _folderTile( title: 'MKV · 자막 (jj_mkv)', @@ -367,6 +371,7 @@ class _SettingsPageState extends State { }, onReset: () => c.updateSettings((x) => x.mkvOutputRoot = null), ), + if (desk) ...[ _folderTile( title: '다운로드 (jj_yt-dlp · jj_aria2)', value: s.downloadRoot, @@ -384,6 +389,7 @@ class _SettingsPageState extends State { title: const Text('복사(Ctrl+C)한 주소 자동 다운로드'), subtitle: const Text('YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)'), ), + ], _section('재생'), ListTile( title: const Text('동영상 하나를 재생할 때'), @@ -396,6 +402,7 @@ class _SettingsPageState extends State { onChanged: (m) => c.updateSettings((x) => x.playlistMode = m!), ), ), + if (desk) ...[ ListTile( title: const Text('탐색기에서 동영상을 열 때'), subtitle: const Text('더블클릭 · 연결 프로그램으로 JJ_MKVMaker 를 골랐을 때 (오른쪽 클릭 메뉴는 그대로)'), @@ -422,6 +429,7 @@ class _SettingsPageState extends State { ), _ExternalPlayers(c: c), _ContextMenuTile(c: c), + ], _section('인터넷 자막 (OpenSubtitles.com)'), _textTile('API 키 (필수, 무료)', s.openSubtitlesKey, '가입 → 프로필 → API consumers → New consumer', @@ -430,6 +438,7 @@ class _SettingsPageState extends State { (v) => c.updateSettings((x) => x.openSubtitlesUser = v.trim())), _textTile('비밀번호 (선택)', s.openSubtitlesPassword, '이 PC 의 설정 파일에 저장됩니다', (v) => c.updateSettings((x) => x.openSubtitlesPassword = v), obscure: true), + if (desk) ...[ ListTile( title: const Text('YouTube 받을 형식'), subtitle: const Text('기본: MP4. 음성만 받으면 MP3 · M4A 로 저장'), @@ -502,6 +511,7 @@ class _SettingsPageState extends State { onChanged: (n) => c.updateSettings((x) => x.maxParallelDownloads = n), ), ), + ], _section('변환'), ListTile( title: const Text('동시 MKV 변환 수'), @@ -511,6 +521,7 @@ class _SettingsPageState extends State { onChanged: (n) => c.updateSettings((x) => x.maxParallelJobs = n), ), ), + if (desk) ...[ _section('실행'), ListTile( title: const Text('종료 (창 닫기 ✕ · 종료 버튼) 를 누르면'), @@ -560,6 +571,7 @@ class _SettingsPageState extends State { title: const Text('완료시 자동 동영상추가'), subtitle: const Text('다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)'), ), + ], _section('AI 자막'), SwitchListTile( value: s.askAiOptions, @@ -587,6 +599,7 @@ class _SettingsPageState extends State { title: const Text('시작할 때 새 버전 확인 (하루 한 번)'), ), ], + if (desk) ...[ ListTile( title: const Text('필수 프로그램 점검'), subtitle: const Text('FFmpeg · yt-dlp · aria2 · Deno 가 없으면 내려받아 설치합니다'), @@ -595,17 +608,18 @@ class _SettingsPageState extends State { child: const Text('점검'), ), ), + ], ListTile( title: const Text('오픈 소스 라이선스'), subtitle: const Text('포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)'), trailing: Row(mainAxisSize: MainAxisSize.min, children: [ - OutlinedButton( + if (desk) OutlinedButton( onPressed: () => c.services.shell.openExternal('system', [ '${File(Platform.resolvedExecutable).parent.path}\\THIRD_PARTY_NOTICES.txt', ]), child: const Text('고지 문서'), ), - const SizedBox(width: 6), + if (desk) const SizedBox(width: 6), OutlinedButton( onPressed: () => showLicensePage(context: context, applicationName: 'JJ_MKVMaker'), child: const Text('패키지 라이선스'), diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/CHANGELOG.md b/packages/ffmpeg_kit_flutter_new_min_gpl/CHANGELOG.md new file mode 100644 index 0000000..66bea8f --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/CHANGELOG.md @@ -0,0 +1,107 @@ +## 2.6.2 + +* **Fixed: Windows build failure `Cannot extract through symlink ... .plugin_symlinks` (#165).** The prebuilt FFmpeg archive was unpacked into the plugin's own source directory. For a package resolved from the pub cache that directory is reached through a symlink, and the extractor refuses to write through it. The archive now unpacks into the application's build directory instead. +* The same change stops the plugin writing ~19 MB of native libraries into the shared pub cache on every platform. `flutter clean` never removed those; to reclaim the space delete `/hosted/pub.dev/-/{linux,windows}/ffmpeg-kit` or run `dart pub cache repair`. The archive is now fetched once per build configuration rather than once per machine. + +## 2.6.1 + +* **Fixed: `FFmpegKitConfig.getPlatform()` returned `windows` on Linux.** The Linux native layer was reporting the wrong platform name, so any code branching on it took the Windows path. It now returns `linux`. The prebuilt libraries attached to the `8.1.2-` release have been rebuilt with the fix; nothing else changed. + +## 2.6.0 + +* **Linux support (x86_64).** `flutter run -d linux` and `flutter build linux` now work. Prebuilt FFmpeg **8.1.2** libraries for the `min-gpl` variant are downloaded at build time, the same way the Windows bundle already worked; set `FFMPEGKIT_LOCAL_DIR` (env or CMake cache variable) to build against a self-built bundle instead. +* Linux builds additionally require **`libjson-glib-dev`** (runtime: `libjson-glib`) alongside Flutter's standard Linux prerequisites. It ships with GNOME, so most desktops already have the runtime. + +## 2.5.5 + +* **Fixed: log/statistics/complete callbacks silently dropped in apps with a second `FlutterEngine`** (e.g. a Firebase Messaging background isolate), a 4.4.1+ regression (#163). FFmpegKit's global native callbacks are now registered once and dispatched to every attached engine that is actually listening, instead of being captured by whichever engine attached last. Background-isolate execution (#155) keeps working. + +## 2.5.4 + +* **Fixed: App Store rejection `Invalid architecture ... arm64e slice with the ios 18.5 SDK` (#164).** The iOS device slices of the bundled FFmpeg XCFrameworks are now arm64 only, for both Swift Package Manager (regenerated binary targets + checksums) and CocoaPods (setup script). arm64e was never used by App Store apps, so nothing is lost. +* SPM only: the regenerated iOS/macOS binaries now actually include the zlib/PNG decoders (#105) and VideoToolbox hardware encoders (#148) that the previous release added for CocoaPods and Android but not for the SPM artifacts. + +## 2.5.3 + +* **Fixed: `Decoder (codec png) not found` (#105).** The native FFmpeg builds for this variant were compiled without zlib, which silently removed the PNG/APNG decoders. zlib is now enabled on Android (`com.antonkarpenko:ffmpeg-kit-*:2.2.2`), iOS and macOS, restoring PNG/APNG decoding (image overlays etc.) as in the original arthenica builds. + +## 2.5.2 + +* **iOS/macOS: hardware-accelerated H.264/HEVC encoding via Apple VideoToolbox** (`h264_videotoolbox`, `hevc_videotoolbox`), plus AVFoundation, is now enabled for this variant. Previously only the `full` / `full_gpl` variants shipped VideoToolbox. No patent-encumbered codecs are added and the codec surface is otherwise unchanged (#148). + +## 2.5.1 + +* Fixed the iOS/macOS build error `'ffmpegkit/FFmpegKitConfig.h' file not found` (#88). The framework setup scripts now download and install atomically, verify the archive, and never leave an empty `Frameworks/` directory behind after a failed download; the podspec re-runs setup until the frameworks are actually present. On restricted networks, point `FFMPEG_KIT_IOS_URL` / `FFMPEG_KIT_MACOS_URL` at a mirror of the release zip. +* Docs: made the FFmpeg badge a link and tidied the README badges. + +## 2.5.0 + +* **Swift Package Manager support** (iOS & macOS): the plugin now integrates via Flutter's SPM support (`flutter config --enable-swift-package-manager`; enabled by default from Flutter 3.44) as well as CocoaPods. Native FFmpeg frameworks are consumed as prebuilt, checksum-pinned XCFrameworks with a native arm64 iOS-simulator slice, downloaded from the GitHub release. +* iOS/macOS plugin sources moved to the Flutter SPM plugin layout; CocoaPods integration is unchanged (and still supported). +* Example app now includes runtime integration tests (`example/integration_test/`). + +## 2.4.2 + +* Fixes the native binary wiring for the CVE-2026-8461 (MagicYUV / "PixelSmash") security patch: Android now pulls `com.antonkarpenko:ffmpeg-kit-*:2.2.1` and iOS/macOS fetch the FFmpeg 8.1.2 frameworks (the 8.1.2 rebuild was previously only wired for Windows). Please upgrade from any earlier version. + +## 2.4.1 + +* **Security (CVE-2026-8461):** updated **FFmpeg to v8.1.2** (arthenica `n8.1.2`) to fix a heap out-of-bounds write in the MagicYUV decoder ("PixelSmash", CVSS 8.8). All platforms (Android/iOS/macOS/Windows) rebuilt against FFmpeg 8.1.2; no API changes. + +## 2.4.0 + +* Updated **FFmpeg to v8.1.1** (arthenica `n8.1.1`), up from 8.0.0. +* iOS/macOS `.xcframework`s, the Android Maven library (`com.antonkarpenko:ffmpeg-kit-*`), and the Windows binaries are now built against FFmpeg 8.1.1. + +## 2.3.2 + +* Android: broadened the bundled ProGuard/R8 consumer rules to keep **all** `com.antonkarpenko.ffmpegkit.**` classes (plus `-dontwarn`), not just the JNI entry points. Fully prevents R8 from stripping FFmpegKit in release builds — fixes the release-only white screen / `channel-error` (FFmpegKit failing to initialise, which cascades into errors like `shared_preferences` `getAll`). Closes #158. + +## 2.3.1 + +* Android: ship ProGuard/R8 **consumer rules** (`consumer-rules.pro`, applied automatically via `consumerProguardFiles`) that keep the FFmpegKit JNI bindings — `FFmpegKitConfig` native/callback methods and `AbiDetect`. Prevents release-mode crashes such as `Bad JNI version returned from JNI_OnLoad` without requiring any ProGuard rules in your app. Thanks @niclasEX (#133). + +## 2.3.0 + +* Fixed **iOS Simulator on Apple Silicon** (`arm64`) support — required for Xcode 26 / iOS 26+ simulators. Builds previously failed with *"The following target(s) do not support arm64 architecture, which is a requirement for Apple Silicon iOS 26+ simulators"*. +* The downloaded iOS `.framework` bundles are now converted to `.xcframework`s at build time (`scripts/setup_ios.sh`), exposing a native `ios-arm64_x86_64-simulator` slice alongside the `ios-arm64_arm64e` device slice. The simulator `arm64` slice is produced by retagging the device `arm64` slice's Mach-O build-version platform to `iOS-Simulator` via `vtool`. +* The iOS podspec now vendors `.xcframework`s and no longer excludes `arm64` for the simulator (`EXCLUDED_ARCHS[sdk=iphonesimulator*]` is now `i386` only). +* If you previously added `config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"` to your app's `ios/Podfile` `post_install` hook as a workaround, **remove it** — it now prevents the simulator `arm64` slice from linking. + +## 2.2.0 + +* Added **Windows** support (x86_64) for the `min-gpl` FFmpeg 8.0 variant (bare FFmpeg plus the four GPL video libraries: `x264`, `x265`, `xvid`, `vid.stab`). +* Native libraries are downloaded automatically at build time from the `8.0.0-min-gpl` release (`ffmpeg-kit-windows-x86_64-min-gpl-8.0.0.zip`); for local development point `FFMPEGKIT_LOCAL_DIR` (CMake cache var or environment) at a locally built bundle. +* The bundled MinGW DLLs are rebased/ASLR-adjusted at build time and the full runtime dependency closure is shipped so the plugin loads standalone. +* Shell scripts are forced to LF via `.gitattributes` so the published package never carries Windows CRLF line endings (avoids the iOS/macOS `pod install` `/bin/bash^M: bad interpreter` failure). + +## 2.1.1 + +* Added ProGuard rules to support release builds +* Enabled ProGuard for Android release builds + +## 2.1.0 + +* Fixed the FFmpeg 8.0 compatibility issue across all platforms. The problem was that `all_channel_counts` was being set AFTER the filter was created, but FFmpeg 8.0 requires it to be set DURING filter creation. + +## 2.0.0 + +* FFmpeg `v8.0.0` with [all the sweet perks](https://ffmpeg.org/index.html#news) + +## 1.1.0 + +* Added proguard-rules.pro to keep `ffmpeg` dependencies when minification is enabled +* Upgraded `freetype` from **2.13.0** to **2.13.3** +* Upgraded `harfbuzz` from **8.0.1** to **11.3.3** +* Upgraded `fontconfig` from **2.16.2** to 2.17.1 +* Added support for `harfbuzz` library in order to support `drawtext` filter +* Fixed missing `libunibreak` for `libass.sh` +* Downgraded required Kotlin version to `v1.8.22` +* Upgraded com.android.library from `8.11.1` to `8.12.0` + +## 1.0.0 + +* Initial release +* FFmpeg version 7.1.1 +* Removed bundled Android FFmpeg (jniLibs, cpp, bindings) +* Added FFmpeg min using new Maven Central package \ No newline at end of file diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/JJ_PATCH.md b/packages/ffmpeg_kit_flutter_new_min_gpl/JJ_PATCH.md new file mode 100644 index 0000000..d5f043b --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/JJ_PATCH.md @@ -0,0 +1,11 @@ +# ffmpeg_kit_flutter_new_min_gpl 2.6.2 사본 (JJ_MKVMaker 패치) + +원본: https://pub.dev/packages/ffmpeg_kit_flutter_new_min_gpl 2.6.2 (pubspec.yaml 의 dependency_overrides 로 사용) + +## 고친 문제 +원본은 Windows 에도 FFmpeg DLL (약 30MB) 을 프로그램 폴더에 넣는다. +JJ_MKVMaker 의 Windows 판은 동봉한 ffmpeg.exe 를 쓰므로 필요 없다. + +## 수정 +- pubspec.yaml: 플러그인 플랫폼을 android 만 남김 (ios · macos · windows · linux 제거) +- windows · linux · macos · ios · example 폴더 제거 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/LICENSE b/packages/ffmpeg_kit_flutter_new_min_gpl/LICENSE new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/README.md b/packages/ffmpeg_kit_flutter_new_min_gpl/README.md new file mode 100644 index 0000000..60e7bfb --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/README.md @@ -0,0 +1,282 @@ +
+ +# 🎬 FFmpegKit for Flutter — Minimal-GPL + +**Run `FFmpeg` & `FFprobe` on Android, iOS, macOS, Windows and Linux from a single Dart API.** + +_A maintained fork of the original [FFmpegKit](https://github.com/arthenica/ffmpeg-kit/tree/main/flutter/flutter), updated for the Android V2 embedding and Flutter 3+._ + +

+ pub version + FFmpeg 8.1.2 + Discord + Buy Me A Coffee +

+ +
+ +--- + +## ✨ Features + +- 🧰 **Full toolkit** — both `FFmpeg` and `FFprobe`, with per-session logs, statistics and callbacks. +- 📱 **Five platforms** — `Android`, `iOS`, `macOS`, `Windows` (x86_64), and `Linux` (x86_64) behind one API. +- 🎞️ **FFmpeg `v8.1.2`** — full command-line-compatible build. +- 🗂️ **Android SAF** — process Storage Access Framework URIs directly. +- 📚 **External libraries** — see the [enabled-libraries table](#-enabled-libraries) below. +- 🔄 **Modernised bindings** for the latest Flutter and Android/macOS toolchains. +- 📦 **Swift Package Manager** — iOS & macOS integrate via SPM (Flutter 3.24+) or CocoaPods, with prebuilt checksum-pinned XCFrameworks. + +## 📦 Install + +```yaml +dependencies: + ffmpeg_kit_flutter_new_min_gpl: ^2.6.0 +``` + +```dart +import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit.dart'; +``` + +## 🎯 Choose your package + +There are **eight** `ffmpeg-kit` packages — pick the smallest one that covers the codecs and features you need. `GPL`-licensed codecs (`x264`, `x265`, `xvidcore`, `vid.stab`) are only in the `-gpl` packages. + +| Package | Best for | +|---|---| +| [`_min`](https://pub.dev/packages/ffmpeg_kit_flutter_new_min) | Smallest build — core FFmpeg only | +| [`_min_gpl`](https://pub.dev/packages/ffmpeg_kit_flutter_new_min_gpl) **(this one)** | Minimal **+ GPL** codecs (x264/x265/xvid/vid.stab) | +| [`_https`](https://pub.dev/packages/ffmpeg_kit_flutter_new_https) | Adds TLS (`gnutls`) for `https://` inputs | +| [`_https_gpl`](https://pub.dev/packages/ffmpeg_kit_flutter_new_https_gpl) | HTTPS **+ GPL** codecs | +| [`_audio`](https://pub.dev/packages/ffmpeg_kit_flutter_new_audio) | Audio-focused (mp3, opus, vorbis, speex, …) | +| [`_video`](https://pub.dev/packages/ffmpeg_kit_flutter_new_video) | Video-focused (dav1d, vpx, theora, webp, …) | +| [`_full`](https://pub.dev/packages/ffmpeg_kit_flutter_new_full) | Everything except GPL codecs | +| [`ffmpeg_kit_flutter_new`](https://pub.dev/packages/ffmpeg_kit_flutter_new) | **Full + GPL** — every library | + +## 🚀 Quick start + +```dart +import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit.dart'; + +final session = await FFmpegKit.execute('-i input.mp4 -c:v mpeg4 output.mp4'); +final returnCode = await session.getReturnCode(); + +if (ReturnCode.isSuccess(returnCode)) { + // ✅ done +} else if (ReturnCode.isCancel(returnCode)) { + // ⏹️ cancelled +} else { + // ❌ error — inspect await session.getLogs() +} +``` + +## 🧩 Enabled libraries + +Below is which system and external libraries each package enables. Some parts of `FFmpeg` are `GPL`-licensed and are only present in the `GPL` packages. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
minmin-gplhttpshttps-gplaudiovideofullfull-gpl
external libraries-vid.stab
x264
x265
xvidcore
gmp
gnutls
gmp
gnutls
vid.stab
x264
x265
xvidcore
lame
libilbc
libvorbis
opencore-amr
opus
shine
soxr
speex
twolame
vo-amrwbenc
dav1d
fontconfig
freetype
fribidi
kvazaar
libass
libiconv
libtheora
libvpx
libwebp
snappy
zimg
dav1d
fontconfig
freetype
fribidi
gmp
gnutls
kvazaar
lame
libass
libiconv
libilbc
libtheora
libvorbis
libvpx
libwebp
libxml2
opencore-amr
opus
shine
snappy
soxr
speex
twolame
vo-amrwbenc
zimg
dav1d
fontconfig
freetype
fribidi
gmp
gnutls
kvazaar
lame
libass
libiconv
libilbc
libtheora
libvorbis
libvpx
libwebp
libxml2
opencore-amr
opus
shine
snappy
soxr
speex
twolame
vid.stab
vo-amrwbenc
x264
x265
xvidcore
zimg
android system librarieszlib
MediaCodec
ios system librariesbzip2
AudioToolbox
AVFoundation
iconv
zlib
ios VideoToolboxVideoToolboxVideoToolbox----VideoToolboxVideoToolbox
macos system librariesbzip2
AudioToolbox
AVFoundation
Core Image
iconv
OpenCL
OpenGL
VideoToolbox
zlib
+ +## 📱 Platform support + + + + + + + + + + + + + + + + + + + + + + +
Android
API Level
Kotlin
Min Version
iOS
Min Target
macOS
Min Target
WindowsLinux
241.8.2214.010.1510+ (x86_64)x86_64
+ +**Architectures** — Android: `arm-v7a`, `arm-v7a-neon`, `arm64-v8a`, `x86`, `x86_64` · iOS: `arm64` (device) + `arm64`/`x86_64` (simulator, incl. Apple Silicon / Xcode 26), shipped as `.xcframework` · macOS: `arm64`, `x86_64`. + +> **Windows:** prebuilt FFmpeg **8.1.2** libraries (x86_64) are downloaded automatically at build time. For local development against a self-built bundle, set `FFMPEGKIT_LOCAL_DIR` (env or CMake cache variable) to the bundle directory before `flutter run/build windows`. + +> **Linux:** prebuilt FFmpeg **8.1.2** libraries (x86_64) are downloaded automatically at build time, exactly as on Windows; `FFMPEGKIT_LOCAL_DIR` works the same way. In addition to Flutter's standard Linux prerequisites (`clang`, `cmake`, `ninja-build`, `pkg-config`, `libgtk-3-dev`), the plugin needs **`libjson-glib-dev`**: +> +> ```bash +> sudo apt-get install libjson-glib-dev # Debian/Ubuntu +> sudo dnf install json-glib-devel # Fedora +> ``` + + +## 📖 Usage + +
+Execute a command & read the result + +```dart +import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit.dart'; + +FFmpegKit.execute('-i file1.mp4 -c:v mpeg4 file2.mp4').then((session) async { + final returnCode = await session.getReturnCode(); + if (ReturnCode.isSuccess(returnCode)) { + // SUCCESS + } else if (ReturnCode.isCancel(returnCode)) { + // CANCEL + } else { + // ERROR + } +}); +``` + +
+ +
+Inspect a session + +```dart +FFmpegKit.execute('-i file1.mp4 -c:v mpeg4 file2.mp4').then((session) async { + final sessionId = session.getSessionId(); + final command = session.getCommand(); + final state = await session.getState(); + final returnCode = await session.getReturnCode(); + final duration = await session.getDuration(); + final output = await session.getOutput(); + final logs = await session.getLogs(); + final statistics = await (session as FFmpegSession).getStatistics(); +}); +``` + +
+ +
+Async execution with callbacks + +```dart +FFmpegKit.executeAsync('-i file1.mp4 -c:v mpeg4 file2.mp4', (Session session) async { + // CALLED WHEN SESSION IS EXECUTED +}, (Log log) { + // CALLED WHEN SESSION PRINTS LOGS +}, (Statistics statistics) { + // CALLED WHEN SESSION GENERATES STATISTICS +}); +``` + +
+ +
+FFprobe & media information + +```dart +FFprobeKit.getMediaInformation('').then((session) async { + final information = await session.getMediaInformation(); +}); +``` + +
+ +
+Cancel sessions + +```dart +FFmpegKit.cancel(); // stop all sessions +FFmpegKit.cancel(sessionId); // stop a specific session +``` + +
+ +
+Android — Storage Access Framework (SAF) + +```dart +// Reading a file +FFmpegKitConfig.selectDocumentForRead('*/*').then((uri) { + FFmpegKitConfig.getSafParameterForRead(uri!).then((safUrl) { + FFmpegKit.executeAsync("-i ${safUrl!} -c:v mpeg4 file2.mp4"); + }); +}); + +// Writing to a file +FFmpegKitConfig.selectDocumentForWrite('video.mp4', 'video/*').then((uri) { + FFmpegKitConfig.getSafParameterForWrite(uri!).then((safUrl) { + FFmpegKit.executeAsync("-i file1.mp4 -c:v mpeg4 ${safUrl}"); + }); +}); +``` + +
+ +
+Global callbacks & fonts + +```dart +FFmpegKitConfig.enableLogCallback((log) { final message = log.getMessage(); }); +FFmpegKitConfig.enableStatisticsCallback((statistics) { final size = statistics.getSize(); }); +FFmpegKitConfig.setFontDirectoryList(["/system/fonts", "/System/Library/Fonts", ""]); +``` + +
+ +## 📄 License + +Licensed under **LGPL 3.0** by default. This package additionally includes `GPL v3.0`-licensed components (`x264`, `x265`, `xvidcore`, `vid.stab`) and is therefore effectively **GPL v3.0**. + +## 💬 Community & support + +- 💙 [Join the Discord](https://discord.gg/8NVwykjA) +- ☕ [Buy me a coffee](https://buymeacoffee.com/sk3llo) +- 🐛 [Report an issue](https://github.com/sk3llo/ffmpeg_kit_flutter/issues) diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/analysis_options.yaml b/packages/ffmpeg_kit_flutter_new_min_gpl/analysis_options.yaml new file mode 100644 index 0000000..90913f0 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/analysis_options.yaml @@ -0,0 +1,63 @@ +analyzer: + strong-mode: + implicit-dynamic: false + errors: + missing_required_param: warning + missing_return: warning + +linter: + rules: + - avoid_catching_errors + - avoid_function_literals_in_foreach_calls + - avoid_private_typedef_functions + - avoid_renaming_method_parameters + - avoid_returning_null_for_void + - avoid_unused_constructor_parameters + - avoid_void_async + - await_only_futures + - camel_case_types + - cancel_subscriptions + - comment_references + - constant_identifier_names + - control_flow_in_finally + - directives_ordering + - empty_statements + - file_names + - hash_and_equals + - implementation_imports + - join_return_with_assignment + - lines_longer_than_80_chars + - missing_whitespace_between_adjacent_strings + - no_runtimeType_toString + - non_constant_identifier_names + - only_throw_errors + - overridden_fields + - package_names + - package_prefixed_library_names + - prefer_asserts_in_initializer_lists + - prefer_const_constructors + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_locals + - prefer_function_declarations_over_variables + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_interpolation_to_compose_strings + - prefer_is_not_operator + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - sort_pub_dependencies + - test_types_in_equals + - throw_in_finally + - unnecessary_brace_in_string_interps + - unnecessary_lambdas + - unnecessary_null_aware_assignments + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_statements + - unnecessary_string_interpolations + - use_string_buffers + - void_checks \ No newline at end of file diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/build.gradle b/packages/ffmpeg_kit_flutter_new_min_gpl/android/build.gradle new file mode 100644 index 0000000..e0b472f --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/build.gradle @@ -0,0 +1,56 @@ +buildscript { + ext.kotlin_version = "2.2.0" + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.11.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "com.antonkarpenko.ffmpegkit" + + compileSdk = 35 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + + buildTypes { + release { + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + defaultConfig { + minSdk = 24 + versionCode 1 + versionName "1.0.0" + consumerProguardFiles 'consumer-rules.pro' + } + + dependencies { + implementation("androidx.annotation:annotation:1.9.1") + implementation("com.antonkarpenko:ffmpeg-kit-min-gpl:2.2.2") + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/consumer-rules.pro b/packages/ffmpeg_kit_flutter_new_min_gpl/android/consumer-rules.pro new file mode 100644 index 0000000..eb5fc76 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/consumer-rules.pro @@ -0,0 +1,17 @@ +# Keep all FFmpegKit classes (incl. JNI entry points and the Flutter plugin) so +# R8/ProGuard never strips them in release builds. Stripping causes native-load +# failures that surface as a white screen / plugin-registration channel errors. +-keep class com.antonkarpenko.ffmpegkit.** { *; } +-dontwarn com.antonkarpenko.ffmpegkit.** + +-keep class com.antonkarpenko.ffmpegkit.FFmpegKitConfig { + native ; + void log(long, int, byte[]); + void statistics(long, int, float, float, long , double, double, double); + int safOpen(int); + int safClose(int); +} + +-keep class com.antonkarpenko.ffmpegkit.AbiDetect { + native ; +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle.properties b/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle.properties new file mode 100644 index 0000000..4d3226a --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle/wrapper/gradle-wrapper.jar b/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 zcma&NWl&^owk(X(xVyW%ySuwf;qI=D6|RlDJ2cR^yEKh!@I- zp9QeisK*rlxC>+~7Dk4IxIRsKBHqdR9b3+fyL=ynHmIDe&|>O*VlvO+%z5;9Z$|DJ zb4dO}-R=MKr^6EKJiOrJdLnCJn>np?~vU-1sSFgPu;pthGwf}bG z(1db%xwr#x)r+`4AGu$j7~u2MpVs3VpLp|mx&;>`0p0vH6kF+D2CY0fVdQOZ@h;A` z{infNyvmFUiu*XG}RNMNwXrbec_*a3N=2zJ|Wh5z* z5rAX$JJR{#zP>KY**>xHTuw?|-Rg|o24V)74HcfVT;WtQHXlE+_4iPE8QE#DUm%x0 zEKr75ur~W%w#-My3Tj`hH6EuEW+8K-^5P62$7Sc5OK+22qj&Pd1;)1#4tKihi=~8C zHiQSst0cpri6%OeaR`PY>HH_;CPaRNty%WTm4{wDK8V6gCZlG@U3$~JQZ;HPvDJcT1V{ z?>H@13MJcCNe#5z+MecYNi@VT5|&UiN1D4ATT+%M+h4c$t;C#UAs3O_q=GxK0}8%8 z8J(_M9bayxN}69ex4dzM_P3oh@ZGREjVvn%%r7=xjkqxJP4kj}5tlf;QosR=%4L5y zWhgejO=vao5oX%mOHbhJ8V+SG&K5dABn6!WiKl{|oPkq(9z8l&Mm%(=qGcFzI=eLu zWc_oCLyf;hVlB@dnwY98?75B20=n$>u3b|NB28H0u-6Rpl((%KWEBOfElVWJx+5yg z#SGqwza7f}$z;n~g%4HDU{;V{gXIhft*q2=4zSezGK~nBgu9-Q*rZ#2f=Q}i2|qOp z!!y4p)4o=LVUNhlkp#JL{tfkhXNbB=Ox>M=n6soptJw-IDI|_$is2w}(XY>a=H52d z3zE$tjPUhWWS+5h=KVH&uqQS=$v3nRs&p$%11b%5qtF}S2#Pc`IiyBIF4%A!;AVoI zXU8-Rpv!DQNcF~(qQnyyMy=-AN~U>#&X1j5BLDP{?K!%h!;hfJI>$mdLSvktEr*89 zdJHvby^$xEX0^l9g$xW-d?J;L0#(`UT~zpL&*cEh$L|HPAu=P8`OQZV!-}l`noSp_ zQ-1$q$R-gDL)?6YaM!=8H=QGW$NT2SeZlb8PKJdc=F-cT@j7Xags+Pr*jPtlHFnf- zh?q<6;)27IdPc^Wdy-mX%2s84C1xZq9Xms+==F4);O`VUASmu3(RlgE#0+#giLh-& zcxm3_e}n4{%|X zJp{G_j+%`j_q5}k{eW&TlP}J2wtZ2^<^E(O)4OQX8FDp6RJq!F{(6eHWSD3=f~(h} zJXCf7=r<16X{pHkm%yzYI_=VDP&9bmI1*)YXZeB}F? z(%QsB5fo*FUZxK$oX~X^69;x~j7ms8xlzpt-T15e9}$4T-pC z6PFg@;B-j|Ywajpe4~bk#S6(fO^|mm1hKOPfA%8-_iGCfICE|=P_~e;Wz6my&)h_~ zkv&_xSAw7AZ%ThYF(4jADW4vg=oEdJGVOs>FqamoL3Np8>?!W#!R-0%2Bg4h?kz5I zKV-rKN2n(vUL%D<4oj@|`eJ>0i#TmYBtYmfla;c!ATW%;xGQ0*TW@PTlGG><@dxUI zg>+3SiGdZ%?5N=8uoLA|$4isK$aJ%i{hECP$bK{J#0W2gQ3YEa zZQ50Stn6hqdfxJ*9#NuSLwKFCUGk@c=(igyVL;;2^wi4o30YXSIb2g_ud$ zgpCr@H0qWtk2hK8Q|&wx)}4+hTYlf;$a4#oUM=V@Cw#!$(nOFFpZ;0lc!qd=c$S}Z zGGI-0jg~S~cgVT=4Vo)b)|4phjStD49*EqC)IPwyeKBLcN;Wu@Aeph;emROAwJ-0< z_#>wVm$)ygH|qyxZaet&(Vf%pVdnvKWJn9`%DAxj3ot;v>S$I}jJ$FLBF*~iZ!ZXE zkvui&p}fI0Y=IDX)mm0@tAd|fEHl~J&K}ZX(Mm3cm1UAuwJ42+AO5@HwYfDH7ipIc zmI;1J;J@+aCNG1M`Btf>YT>~c&3j~Qi@Py5JT6;zjx$cvOQW@3oQ>|}GH?TW-E z1R;q^QFjm5W~7f}c3Ww|awg1BAJ^slEV~Pk`Kd`PS$7;SqJZNj->it4DW2l15}xP6 zoCl$kyEF%yJni0(L!Z&14m!1urXh6Btj_5JYt1{#+H8w?5QI%% zo-$KYWNMJVH?Hh@1n7OSu~QhSswL8x0=$<8QG_zepi_`y_79=nK=_ZP_`Em2UI*tyQoB+r{1QYZCpb?2OrgUw#oRH$?^Tj!Req>XiE#~B|~ z+%HB;=ic+R@px4Ld8mwpY;W^A%8%l8$@B@1m5n`TlKI6bz2mp*^^^1mK$COW$HOfp zUGTz-cN9?BGEp}5A!mDFjaiWa2_J2Iq8qj0mXzk; z66JBKRP{p%wN7XobR0YjhAuW9T1Gw3FDvR5dWJ8ElNYF94eF3ebu+QwKjtvVu4L zI9ip#mQ@4uqVdkl-TUQMb^XBJVLW(-$s;Nq;@5gr4`UfLgF$adIhd?rHOa%D);whv z=;krPp~@I+-Z|r#s3yCH+c1US?dnm+C*)r{m+86sTJusLdNu^sqLrfWed^ndHXH`m zd3#cOe3>w-ga(Dus_^ppG9AC>Iq{y%%CK+Cro_sqLCs{VLuK=dev>OL1dis4(PQ5R zcz)>DjEkfV+MO;~>VUlYF00SgfUo~@(&9$Iy2|G0T9BSP?&T22>K46D zL*~j#yJ?)^*%J3!16f)@Y2Z^kS*BzwfAQ7K96rFRIh>#$*$_Io;z>ux@}G98!fWR@ zGTFxv4r~v)Gsd|pF91*-eaZ3Qw1MH$K^7JhWIdX%o$2kCbvGDXy)a?@8T&1dY4`;L z4Kn+f%SSFWE_rpEpL9bnlmYq`D!6F%di<&Hh=+!VI~j)2mfil03T#jJ_s?}VV0_hp z7T9bWxc>Jm2Z0WMU?`Z$xE74Gu~%s{mW!d4uvKCx@WD+gPUQ zV0vQS(Ig++z=EHN)BR44*EDSWIyT~R4$FcF*VEY*8@l=218Q05D2$|fXKFhRgBIEE zdDFB}1dKkoO^7}{5crKX!p?dZWNz$m>1icsXG2N+((x0OIST9Zo^DW_tytvlwXGpn zs8?pJXjEG;T@qrZi%#h93?FP$!&P4JA(&H61tqQi=opRzNpm zkrG}$^t9&XduK*Qa1?355wd8G2CI6QEh@Ua>AsD;7oRUNLPb76m4HG3K?)wF~IyS3`fXuNM>${?wmB zpVz;?6_(Fiadfd{vUCBM*_kt$+F3J+IojI;9L(gc9n3{sEZyzR9o!_mOwFC#tQ{Q~ zP3-`#uK#tP3Q7~Q;4H|wjZHO8h7e4IuBxl&vz2w~D8)w=Wtg31zpZhz%+kzSzL*dV zwp@{WU4i;hJ7c2f1O;7Mz6qRKeASoIv0_bV=i@NMG*l<#+;INk-^`5w@}Dj~;k=|}qM1vq_P z|GpBGe_IKq|LNy9SJhKOQ$c=5L{Dv|Q_lZl=-ky*BFBJLW9&y_C|!vyM~rQx=!vun z?rZJQB5t}Dctmui5i31C_;_}CEn}_W%>oSXtt>@kE1=JW*4*v4tPp;O6 zmAk{)m!)}34pTWg8{i>($%NQ(Tl;QC@J@FfBoc%Gr&m560^kgSfodAFrIjF}aIw)X zoXZ`@IsMkc8_=w%-7`D6Y4e*CG8k%Ud=GXhsTR50jUnm+R*0A(O3UKFg0`K;qp1bl z7``HN=?39ic_kR|^R^~w-*pa?Vj#7|e9F1iRx{GN2?wK!xR1GW!qa=~pjJb-#u1K8 zeR?Y2i-pt}yJq;SCiVHODIvQJX|ZJaT8nO+(?HXbLefulKKgM^B(UIO1r+S=7;kLJ zcH}1J=Px2jsh3Tec&v8Jcbng8;V-`#*UHt?hB(pmOipKwf3Lz8rG$heEB30Sg*2rx zV<|KN86$soN(I!BwO`1n^^uF2*x&vJ$2d$>+`(romzHP|)K_KkO6Hc>_dwMW-M(#S zK(~SiXT1@fvc#U+?|?PniDRm01)f^#55;nhM|wi?oG>yBsa?~?^xTU|fX-R(sTA+5 zaq}-8Tx7zrOy#3*JLIIVsBmHYLdD}!0NP!+ITW+Thn0)8SS!$@)HXwB3tY!fMxc#1 zMp3H?q3eD?u&Njx4;KQ5G>32+GRp1Ee5qMO0lZjaRRu&{W<&~DoJNGkcYF<5(Ab+J zgO>VhBl{okDPn78<%&e2mR{jwVCz5Og;*Z;;3%VvoGo_;HaGLWYF7q#jDX=Z#Ml`H z858YVV$%J|e<1n`%6Vsvq7GmnAV0wW4$5qQ3uR@1i>tW{xrl|ExywIc?fNgYlA?C5 zh$ezAFb5{rQu6i7BSS5*J-|9DQ{6^BVQ{b*lq`xS@RyrsJN?-t=MTMPY;WYeKBCNg z^2|pN!Q^WPJuuO4!|P@jzt&tY1Y8d%FNK5xK(!@`jO2aEA*4 zkO6b|UVBipci?){-Ke=+1;mGlND8)6+P;8sq}UXw2hn;fc7nM>g}GSMWu&v&fqh

iViYT=fZ(|3Ox^$aWPp4a8h24tD<|8-!aK0lHgL$N7Efw}J zVIB!7=T$U`ao1?upi5V4Et*-lTG0XvExbf!ya{cua==$WJyVG(CmA6Of*8E@DSE%L z`V^$qz&RU$7G5mg;8;=#`@rRG`-uS18$0WPN@!v2d{H2sOqP|!(cQ@ zUHo!d>>yFArLPf1q`uBvY32miqShLT1B@gDL4XoVTK&@owOoD)OIHXrYK-a1d$B{v zF^}8D3Y^g%^cnvScOSJR5QNH+BI%d|;J;wWM3~l>${fb8DNPg)wrf|GBP8p%LNGN# z3EaIiItgwtGgT&iYCFy9-LG}bMI|4LdmmJt@V@% zb6B)1kc=T)(|L@0;wr<>=?r04N;E&ef+7C^`wPWtyQe(*pD1pI_&XHy|0gIGHMekd zF_*M4yi6J&Z4LQj65)S zXwdM{SwUo%3SbPwFsHgqF@V|6afT|R6?&S;lw=8% z3}@9B=#JI3@B*#4s!O))~z zc>2_4Q_#&+5V`GFd?88^;c1i7;Vv_I*qt!_Yx*n=;rj!82rrR2rQ8u5(Ejlo{15P% zs~!{%XJ>FmJ})H^I9bn^Re&38H{xA!0l3^89k(oU;bZWXM@kn$#aoS&Y4l^-WEn-fH39Jb9lA%s*WsKJQl?n9B7_~P z-XM&WL7Z!PcoF6_D>V@$CvUIEy=+Z&0kt{szMk=f1|M+r*a43^$$B^MidrT0J;RI` z(?f!O<8UZkm$_Ny$Hth1J#^4ni+im8M9mr&k|3cIgwvjAgjH z8`N&h25xV#v*d$qBX5jkI|xOhQn!>IYZK7l5#^P4M&twe9&Ey@@GxYMxBZq2e7?`q z$~Szs0!g{2fGcp9PZEt|rdQ6bhAgpcLHPz?f-vB?$dc*!9OL?Q8mn7->bFD2Si60* z!O%y)fCdMSV|lkF9w%x~J*A&srMyYY3{=&$}H zGQ4VG_?$2X(0|vT0{=;W$~icCI{b6W{B!Q8xdGhF|D{25G_5_+%s(46lhvNLkik~R z>nr(&C#5wwOzJZQo9m|U<;&Wk!_#q|V>fsmj1g<6%hB{jGoNUPjgJslld>xmODzGjYc?7JSuA?A_QzjDw5AsRgi@Y|Z0{F{!1=!NES-#*f^s4l0Hu zz468))2IY5dmD9pa*(yT5{EyP^G>@ZWumealS-*WeRcZ}B%gxq{MiJ|RyX-^C1V=0 z@iKdrGi1jTe8Ya^x7yyH$kBNvM4R~`fbPq$BzHum-3Zo8C6=KW@||>zsA8-Y9uV5V z#oq-f5L5}V<&wF4@X@<3^C%ptp6+Ce)~hGl`kwj)bsAjmo_GU^r940Z-|`<)oGnh7 zFF0Tde3>ui?8Yj{sF-Z@)yQd~CGZ*w-6p2U<8}JO-sRsVI5dBji`01W8A&3$?}lxBaC&vn0E$c5tW* zX>5(zzZ=qn&!J~KdsPl;P@bmA-Pr8T*)eh_+Dv5=Ma|XSle6t(k8qcgNyar{*ReQ8 zTXwi=8vr>!3Ywr+BhggHDw8ke==NTQVMCK`$69fhzEFB*4+H9LIvdt-#IbhZvpS}} zO3lz;P?zr0*0$%-Rq_y^k(?I{Mk}h@w}cZpMUp|ucs55bcloL2)($u%mXQw({Wzc~ z;6nu5MkjP)0C(@%6Q_I_vsWrfhl7Zpoxw#WoE~r&GOSCz;_ro6i(^hM>I$8y>`!wW z*U^@?B!MMmb89I}2(hcE4zN2G^kwyWCZp5JG>$Ez7zP~D=J^LMjSM)27_0B_X^C(M z`fFT+%DcKlu?^)FCK>QzSnV%IsXVcUFhFdBP!6~se&xxrIxsvySAWu++IrH;FbcY$ z2DWTvSBRfLwdhr0nMx+URA$j3i7_*6BWv#DXfym?ZRDcX9C?cY9sD3q)uBDR3uWg= z(lUIzB)G$Hr!){>E{s4Dew+tb9kvToZp-1&c?y2wn@Z~(VBhqz`cB;{E4(P3N2*nJ z_>~g@;UF2iG{Kt(<1PyePTKahF8<)pozZ*xH~U-kfoAayCwJViIrnqwqO}7{0pHw$ zs2Kx?s#vQr7XZ264>5RNKSL8|Ty^=PsIx^}QqOOcfpGUU4tRkUc|kc7-!Ae6!+B{o~7nFpm3|G5^=0#Bnm6`V}oSQlrX(u%OWnC zoLPy&Q;1Jui&7ST0~#+}I^&?vcE*t47~Xq#YwvA^6^} z`WkC)$AkNub|t@S!$8CBlwbV~?yp&@9h{D|3z-vJXgzRC5^nYm+PyPcgRzAnEi6Q^gslXYRv4nycsy-SJu?lMps-? zV`U*#WnFsdPLL)Q$AmD|0`UaC4ND07+&UmOu!eHruzV|OUox<+Jl|Mr@6~C`T@P%s zW7sgXLF2SSe9Fl^O(I*{9wsFSYb2l%-;&Pi^dpv!{)C3d0AlNY6!4fgmSgj_wQ*7Am7&$z;Jg&wgR-Ih;lUvWS|KTSg!&s_E9_bXBkZvGiC6bFKDWZxsD$*NZ#_8bl zG1P-#@?OQzED7@jlMJTH@V!6k;W>auvft)}g zhoV{7$q=*;=l{O>Q4a@ ziMjf_u*o^PsO)#BjC%0^h>Xp@;5$p{JSYDt)zbb}s{Kbt!T*I@Pk@X0zds6wsefuU zW$XY%yyRGC94=6mf?x+bbA5CDQ2AgW1T-jVAJbm7K(gp+;v6E0WI#kuACgV$r}6L? zd|Tj?^%^*N&b>Dd{Wr$FS2qI#Ucs1yd4N+RBUQiSZGujH`#I)mG&VKoDh=KKFl4=G z&MagXl6*<)$6P}*Tiebpz5L=oMaPrN+caUXRJ`D?=K9!e0f{@D&cZLKN?iNP@X0aF zE(^pl+;*T5qt?1jRC=5PMgV!XNITRLS_=9{CJExaQj;lt!&pdzpK?8p>%Mb+D z?yO*uSung=-`QQ@yX@Hyd4@CI^r{2oiu`%^bNkz+Nkk!IunjwNC|WcqvX~k=><-I3 zDQdbdb|!v+Iz01$w@aMl!R)koD77Xp;eZwzSl-AT zr@Vu{=xvgfq9akRrrM)}=!=xcs+U1JO}{t(avgz`6RqiiX<|hGG1pmop8k6Q+G_mv zJv|RfDheUp2L3=^C=4aCBMBn0aRCU(DQwX-W(RkRwmLeuJYF<0urcaf(=7)JPg<3P zQs!~G)9CT18o!J4{zX{_e}4eS)U-E)0FAt}wEI(c0%HkxgggW;(1E=>J17_hsH^sP z%lT0LGgbUXHx-K*CI-MCrP66UP0PvGqM$MkeLyqHdbgP|_Cm!7te~b8p+e6sQ_3k| zVcwTh6d83ltdnR>D^)BYQpDKlLk3g0Hdcgz2}%qUs9~~Rie)A-BV1mS&naYai#xcZ z(d{8=-LVpTp}2*y)|gR~;qc7fp26}lPcLZ#=JpYcn3AT9(UIdOyg+d(P5T7D&*P}# zQCYplZO5|7+r19%9e`v^vfSS1sbX1c%=w1;oyruXB%Kl$ACgKQ6=qNWLsc=28xJjg zwvsI5-%SGU|3p>&zXVl^vVtQT3o-#$UT9LI@Npz~6=4!>mc431VRNN8od&Ul^+G_kHC`G=6WVWM z%9eWNyy(FTO|A+@x}Ou3CH)oi;t#7rAxdIXfNFwOj_@Y&TGz6P_sqiB`Q6Lxy|Q{`|fgmRG(k+!#b*M+Z9zFce)f-7;?Km5O=LHV9f9_87; zF7%R2B+$?@sH&&-$@tzaPYkw0;=i|;vWdI|Wl3q_Zu>l;XdIw2FjV=;Mq5t1Q0|f< zs08j54Bp`3RzqE=2enlkZxmX6OF+@|2<)A^RNQpBd6o@OXl+i)zO%D4iGiQNuXd+zIR{_lb96{lc~bxsBveIw6umhShTX+3@ZJ=YHh@ zWY3(d0azg;7oHn>H<>?4@*RQbi>SmM=JrHvIG(~BrvI)#W(EAeO6fS+}mxxcc+X~W6&YVl86W9WFSS}Vz-f9vS?XUDBk)3TcF z8V?$4Q)`uKFq>xT=)Y9mMFVTUk*NIA!0$?RP6Ig0TBmUFrq*Q-Agq~DzxjStQyJ({ zBeZ;o5qUUKg=4Hypm|}>>L=XKsZ!F$yNTDO)jt4H0gdQ5$f|d&bnVCMMXhNh)~mN z@_UV6D7MVlsWz+zM+inZZp&P4fj=tm6fX)SG5H>OsQf_I8c~uGCig$GzuwViK54bcgL;VN|FnyQl>Ed7(@>=8$a_UKIz|V6CeVSd2(P z0Uu>A8A+muM%HLFJQ9UZ5c)BSAv_zH#1f02x?h9C}@pN@6{>UiAp>({Fn(T9Q8B z^`zB;kJ5b`>%dLm+Ol}ty!3;8f1XDSVX0AUe5P#@I+FQ-`$(a;zNgz)4x5hz$Hfbg z!Q(z26wHLXko(1`;(BAOg_wShpX0ixfWq3ponndY+u%1gyX)_h=v1zR#V}#q{au6; z!3K=7fQwnRfg6FXtNQmP>`<;!N137paFS%y?;lb1@BEdbvQHYC{976l`cLqn;b8lp zIDY>~m{gDj(wfnK!lpW6pli)HyLEiUrNc%eXTil|F2s(AY+LW5hkKb>TQ3|Q4S9rr zpDs4uK_co6XPsn_z$LeS{K4jFF`2>U`tbgKdyDne`xmR<@6AA+_hPNKCOR-Zqv;xk zu5!HsBUb^!4uJ7v0RuH-7?l?}b=w5lzzXJ~gZcxRKOovSk@|#V+MuX%Y+=;14i*%{)_gSW9(#4%)AV#3__kac1|qUy!uyP{>?U#5wYNq}y$S9pCc zFc~4mgSC*G~j0u#qqp9 z${>3HV~@->GqEhr_Xwoxq?Hjn#=s2;i~g^&Hn|aDKpA>Oc%HlW(KA1?BXqpxB;Ydx)w;2z^MpjJ(Qi(X!$5RC z*P{~%JGDQqojV>2JbEeCE*OEu!$XJ>bWA9Oa_Hd;y)F%MhBRi*LPcdqR8X`NQ&1L# z5#9L*@qxrx8n}LfeB^J{%-?SU{FCwiWyHp682F+|pa+CQa3ZLzBqN1{)h4d6+vBbV zC#NEbQLC;}me3eeYnOG*nXOJZEU$xLZ1<1Y=7r0(-U0P6-AqwMAM`a(Ed#7vJkn6plb4eI4?2y3yOTGmmDQ!z9`wzbf z_OY#0@5=bnep;MV0X_;;SJJWEf^E6Bd^tVJ9znWx&Ks8t*B>AM@?;D4oWUGc z!H*`6d7Cxo6VuyS4Eye&L1ZRhrRmN6Lr`{NL(wDbif|y&z)JN>Fl5#Wi&mMIr5i;x zBx}3YfF>>8EC(fYnmpu~)CYHuHCyr5*`ECap%t@y=jD>!_%3iiE|LN$mK9>- zHdtpy8fGZtkZF?%TW~29JIAfi2jZT8>OA7=h;8T{{k?c2`nCEx9$r zS+*&vt~2o^^J+}RDG@+9&M^K*z4p{5#IEVbz`1%`m5c2};aGt=V?~vIM}ZdPECDI)47|CWBCfDWUbxBCnmYivQ*0Nu_xb*C>~C9(VjHM zxe<*D<#dQ8TlpMX2c@M<9$w!RP$hpG4cs%AI){jp*Sj|*`m)5(Bw*A0$*i-(CA5#%>a)$+jI2C9r6|(>J8InryENI z$NohnxDUB;wAYDwrb*!N3noBTKPpPN}~09SEL18tkG zxgz(RYU_;DPT{l?Q$+eaZaxnsWCA^ds^0PVRkIM%bOd|G2IEBBiz{&^JtNsODs;5z zICt_Zj8wo^KT$7Bg4H+y!Df#3mbl%%?|EXe!&(Vmac1DJ*y~3+kRKAD=Ovde4^^%~ zw<9av18HLyrf*_>Slp;^i`Uy~`mvBjZ|?Ad63yQa#YK`4+c6;pW4?XIY9G1(Xh9WO8{F-Aju+nS9Vmv=$Ac0ienZ+p9*O%NG zMZKy5?%Z6TAJTE?o5vEr0r>f>hb#2w2U3DL64*au_@P!J!TL`oH2r*{>ffu6|A7tv zL4juf$DZ1MW5ZPsG!5)`k8d8c$J$o;%EIL0va9&GzWvkS%ZsGb#S(?{!UFOZ9<$a| zY|a+5kmD5N&{vRqkgY>aHsBT&`rg|&kezoD)gP0fsNYHsO#TRc_$n6Lf1Z{?+DLziXlHrq4sf(!>O{?Tj;Eh@%)+nRE_2VxbN&&%%caU#JDU%vL3}Cb zsb4AazPI{>8H&d=jUaZDS$-0^AxE@utGs;-Ez_F(qC9T=UZX=>ok2k2 ziTn{K?y~a5reD2A)P${NoI^>JXn>`IeArow(41c-Wm~)wiryEP(OS{YXWi7;%dG9v zI?mwu1MxD{yp_rrk!j^cKM)dc4@p4Ezyo%lRN|XyD}}>v=Xoib0gOcdXrQ^*61HNj z=NP|pd>@yfvr-=m{8$3A8TQGMTE7g=z!%yt`8`Bk-0MMwW~h^++;qyUP!J~ykh1GO z(FZ59xuFR$(WE;F@UUyE@Sp>`aVNjyj=Ty>_Vo}xf`e7`F;j-IgL5`1~-#70$9_=uBMq!2&1l zomRgpD58@)YYfvLtPW}{C5B35R;ZVvB<<#)x%srmc_S=A7F@DW8>QOEGwD6suhwCg z>Pa+YyULhmw%BA*4yjDp|2{!T98~<6Yfd(wo1mQ!KWwq0eg+6)o1>W~f~kL<-S+P@$wx*zeI|1t7z#Sxr5 zt6w+;YblPQNplq4Z#T$GLX#j6yldXAqj>4gAnnWtBICUnA&-dtnlh=t0Ho_vEKwV` z)DlJi#!@nkYV#$!)@>udAU*hF?V`2$Hf=V&6PP_|r#Iv*J$9)pF@X3`k;5})9^o4y z&)~?EjX5yX12O(BsFy-l6}nYeuKkiq`u9145&3Ssg^y{5G3Pse z9w(YVa0)N-fLaBq1`P!_#>SS(8fh_5!f{UrgZ~uEdeMJIz7DzI5!NHHqQtm~#CPij z?=N|J>nPR6_sL7!f4hD_|KH`vf8(Wpnj-(gPWH+ZvID}%?~68SwhPTC3u1_cB`otq z)U?6qo!ZLi5b>*KnYHWW=3F!p%h1;h{L&(Q&{qY6)_qxNfbP6E3yYpW!EO+IW3?@J z);4>g4gnl^8klu7uA>eGF6rIGSynacogr)KUwE_R4E5Xzi*Qir@b-jy55-JPC8c~( zo!W8y9OGZ&`xmc8;=4-U9=h{vCqfCNzYirONmGbRQlR`WWlgnY+1wCXbMz&NT~9*| z6@FrzP!LX&{no2!Ln_3|I==_4`@}V?4a;YZKTdw;vT<+K+z=uWbW(&bXEaWJ^W8Td z-3&1bY^Z*oM<=M}LVt>_j+p=2Iu7pZmbXrhQ_k)ysE9yXKygFNw$5hwDn(M>H+e1&9BM5!|81vd%r%vEm zqxY3?F@fb6O#5UunwgAHR9jp_W2zZ}NGp2%mTW@(hz7$^+a`A?mb8|_G*GNMJ) zjqegXQio=i@AINre&%ofexAr95aop5C+0MZ0m-l=MeO8m3epm7U%vZB8+I+C*iNFM z#T3l`gknX;D$-`2XT^Cg*vrv=RH+P;_dfF++cP?B_msQI4j+lt&rX2)3GaJx%W*Nn zkML%D{z5tpHH=dksQ*gzc|}gzW;lwAbxoR07VNgS*-c3d&8J|;@3t^ zVUz*J*&r7DFRuFVDCJDK8V9NN5hvpgGjwx+5n)qa;YCKe8TKtdnh{I7NU9BCN!0dq zczrBk8pE{{@vJa9ywR@mq*J=v+PG;?fwqlJVhijG!3VmIKs>9T6r7MJpC)m!Tc#>g zMtVsU>wbwFJEfwZ{vB|ZlttNe83)$iz`~#8UJ^r)lJ@HA&G#}W&ZH*;k{=TavpjWE z7hdyLZPf*X%Gm}i`Y{OGeeu^~nB8=`{r#TUrM-`;1cBvEd#d!kPqIgYySYhN-*1;L z^byj%Yi}Gx)Wnkosi337BKs}+5H5dth1JA{Ir-JKN$7zC)*}hqeoD(WfaUDPT>0`- z(6sa0AoIqASwF`>hP}^|)a_j2s^PQn*qVC{Q}htR z5-)duBFXT_V56-+UohKXlq~^6uf!6sA#ttk1o~*QEy_Y-S$gAvq47J9Vtk$5oA$Ct zYhYJ@8{hsC^98${!#Ho?4y5MCa7iGnfz}b9jE~h%EAAv~Qxu)_rAV;^cygV~5r_~?l=B`zObj7S=H=~$W zPtI_m%g$`kL_fVUk9J@>EiBH zOO&jtn~&`hIFMS5S`g8w94R4H40mdNUH4W@@XQk1sr17b{@y|JB*G9z1|CrQjd+GX z6+KyURG3;!*BQrentw{B2R&@2&`2}n(z-2&X7#r!{yg@Soy}cRD~j zj9@UBW+N|4HW4AWapy4wfUI- zZ`gSL6DUlgj*f1hSOGXG0IVH8HxK?o2|3HZ;KW{K+yPAlxtb)NV_2AwJm|E)FRs&& z=c^e7bvUsztY|+f^k7NXs$o1EUq>cR7C0$UKi6IooHWlK_#?IWDkvywnzg&ThWo^? z2O_N{5X39#?eV9l)xI(>@!vSB{DLt*oY!K1R8}_?%+0^C{d9a%N4 zoxHVT1&Lm|uDX%$QrBun5e-F`HJ^T$ zmzv)p@4ZHd_w9!%Hf9UYNvGCw2TTTbrj9pl+T9%-_-}L(tES>Or-}Z4F*{##n3~L~TuxjirGuIY#H7{%$E${?p{Q01 zi6T`n;rbK1yIB9jmQNycD~yZq&mbIsFWHo|ZAChSFPQa<(%d8mGw*V3fh|yFoxOOiWJd(qvVb!Z$b88cg->N=qO*4k~6;R==|9ihg&riu#P~s4Oap9O7f%crSr^rljeIfXDEg>wi)&v*a%7zpz<9w z*r!3q9J|390x`Zk;g$&OeN&ctp)VKRpDSV@kU2Q>jtok($Y-*x8_$2piTxun81@vt z!Vj?COa0fg2RPXMSIo26T=~0d`{oGP*eV+$!0I<(4azk&Vj3SiG=Q!6mX0p$z7I}; z9BJUFgT-K9MQQ-0@Z=^7R<{bn2Fm48endsSs`V7_@%8?Bxkqv>BDoVcj?K#dV#uUP zL1ND~?D-|VGKe3Rw_7-Idpht>H6XRLh*U7epS6byiGvJpr%d}XwfusjH9g;Z98H`x zyde%%5mhGOiL4wljCaWCk-&uE4_OOccb9c!ZaWt4B(wYl!?vyzl%7n~QepN&eFUrw zFIOl9c({``6~QD+43*_tzP{f2x41h(?b43^y6=iwyB)2os5hBE!@YUS5?N_tXd=h( z)WE286Fbd>R4M^P{!G)f;h<3Q>Fipuy+d2q-)!RyTgt;wr$(?9ox3;q+{E*ZQHhOn;lM`cjnu9 zXa48ks-v(~b*;MAI<>YZH(^NV8vjb34beE<_cwKlJoR;k6lJNSP6v}uiyRD?|0w+X@o1ONrH8a$fCxXpf? z?$DL0)7|X}Oc%h^zrMKWc-NS9I0Utu@>*j}b@tJ=ixQSJ={4@854wzW@E>VSL+Y{i z#0b=WpbCZS>kUCO_iQz)LoE>P5LIG-hv9E+oG}DtlIDF>$tJ1aw9^LuhLEHt?BCj& z(O4I8v1s#HUi5A>nIS-JK{v!7dJx)^Yg%XjNmlkWAq2*cv#tHgz`Y(bETc6CuO1VkN^L-L3j_x<4NqYb5rzrLC-7uOv z!5e`GZt%B782C5-fGnn*GhDF$%(qP<74Z}3xx+{$4cYKy2ikxI7B2N+2r07DN;|-T->nU&!=Cm#rZt%O_5c&1Z%nlWq3TKAW0w zQqemZw_ue--2uKQsx+niCUou?HjD`xhEjjQd3%rrBi82crq*~#uA4+>vR<_S{~5ce z-2EIl?~s z1=GVL{NxP1N3%=AOaC}j_Fv=ur&THz zyO!d9kHq|c73kpq`$+t+8Bw7MgeR5~`d7ChYyGCBWSteTB>8WAU(NPYt2Dk`@#+}= zI4SvLlyk#pBgVigEe`?NG*vl7V6m+<}%FwPV=~PvvA)=#ths==DRTDEYh4V5}Cf$z@#;< zyWfLY_5sP$gc3LLl2x+Ii)#b2nhNXJ{R~vk`s5U7Nyu^3yFg&D%Txwj6QezMX`V(x z=C`{76*mNb!qHHs)#GgGZ_7|vkt9izl_&PBrsu@}L`X{95-2jf99K)0=*N)VxBX2q z((vkpP2RneSIiIUEnGb?VqbMb=Zia+rF~+iqslydE34cSLJ&BJW^3knX@M;t*b=EA zNvGzv41Ld_T+WT#XjDB840vovUU^FtN_)G}7v)1lPetgpEK9YS^OWFkPoE{ovj^=@ zO9N$S=G$1ecndT_=5ehth2Lmd1II-PuT~C9`XVePw$y8J#dpZ?Tss<6wtVglm(Ok7 z3?^oi@pPio6l&!z8JY(pJvG=*pI?GIOu}e^EB6QYk$#FJQ%^AIK$I4epJ+9t?KjqA+bkj&PQ*|vLttme+`9G=L% ziadyMw_7-M)hS(3E$QGNCu|o23|%O+VN7;Qggp?PB3K-iSeBa2b}V4_wY`G1Jsfz4 z9|SdB^;|I8E8gWqHKx!vj_@SMY^hLEIbSMCuE?WKq=c2mJK z8LoG-pnY!uhqFv&L?yEuxo{dpMTsmCn)95xanqBrNPTgXP((H$9N${Ow~Is-FBg%h z53;|Y5$MUN)9W2HBe2TD`ct^LHI<(xWrw}$qSoei?}s)&w$;&!14w6B6>Yr6Y8b)S z0r71`WmAvJJ`1h&poLftLUS6Ir zC$bG9!Im_4Zjse)#K=oJM9mHW1{%l8sz$1o?ltdKlLTxWWPB>Vk22czVt|1%^wnN@*!l)}?EgtvhC>vlHm^t+ogpgHI1_$1ox9e;>0!+b(tBrmXRB`PY1vp-R**8N7 zGP|QqI$m(Rdu#=(?!(N}G9QhQ%o!aXE=aN{&wtGP8|_qh+7a_j_sU5|J^)vxq;# zjvzLn%_QPHZZIWu1&mRAj;Sa_97p_lLq_{~j!M9N^1yp3U_SxRqK&JnR%6VI#^E12 z>CdOVI^_9aPK2eZ4h&^{pQs}xsijXgFYRIxJ~N7&BB9jUR1fm!(xl)mvy|3e6-B3j zJn#ajL;bFTYJ2+Q)tDjx=3IklO@Q+FFM}6UJr6km7hj7th9n_&JR7fnqC!hTZoM~T zBeaVFp%)0cbPhejX<8pf5HyRUj2>aXnXBqDJe73~J%P(2C?-RT{c3NjE`)om! zl$uewSgWkE66$Kb34+QZZvRn`fob~Cl9=cRk@Es}KQm=?E~CE%spXaMO6YmrMl%9Q zlA3Q$3|L1QJ4?->UjT&CBd!~ru{Ih^in&JXO=|<6J!&qp zRe*OZ*cj5bHYlz!!~iEKcuE|;U4vN1rk$xq6>bUWD*u(V@8sG^7>kVuo(QL@Ki;yL zWC!FT(q{E8#on>%1iAS0HMZDJg{Z{^!De(vSIq&;1$+b)oRMwA3nc3mdTSG#3uYO_ z>+x;7p4I;uHz?ZB>dA-BKl+t-3IB!jBRgdvAbW!aJ(Q{aT>+iz?91`C-xbe)IBoND z9_Xth{6?(y3rddwY$GD65IT#f3<(0o#`di{sh2gm{dw*#-Vnc3r=4==&PU^hCv$qd zjw;>i&?L*Wq#TxG$mFIUf>eK+170KG;~+o&1;Tom9}}mKo23KwdEM6UonXgc z!6N(@k8q@HPw{O8O!lAyi{rZv|DpgfU{py+j(X_cwpKqcalcqKIr0kM^%Br3SdeD> zHSKV94Yxw;pjzDHo!Q?8^0bb%L|wC;4U^9I#pd5O&eexX+Im{ z?jKnCcsE|H?{uGMqVie_C~w7GX)kYGWAg%-?8|N_1#W-|4F)3YTDC+QSq1s!DnOML3@d`mG%o2YbYd#jww|jD$gotpa)kntakp#K;+yo-_ZF9qrNZw<%#C zuPE@#3RocLgPyiBZ+R_-FJ_$xP!RzWm|aN)S+{$LY9vvN+IW~Kf3TsEIvP+B9Mtm! zpfNNxObWQpLoaO&cJh5>%slZnHl_Q~(-Tfh!DMz(dTWld@LG1VRF`9`DYKhyNv z2pU|UZ$#_yUx_B_|MxUq^glT}O5Xt(Vm4Mr02><%C)@v;vPb@pT$*yzJ4aPc_FZ3z z3}PLoMBIM>q_9U2rl^sGhk1VUJ89=*?7|v`{!Z{6bqFMq(mYiA?%KbsI~JwuqVA9$H5vDE+VocjX+G^%bieqx->s;XWlKcuv(s%y%D5Xbc9+ zc(_2nYS1&^yL*ey664&4`IoOeDIig}y-E~_GS?m;D!xv5-xwz+G`5l6V+}CpeJDi^ z%4ed$qowm88=iYG+(`ld5Uh&>Dgs4uPHSJ^TngXP_V6fPyl~>2bhi20QB%lSd#yYn zO05?KT1z@?^-bqO8Cg`;ft>ilejsw@2%RR7;`$Vs;FmO(Yr3Fp`pHGr@P2hC%QcA|X&N2Dn zYf`MqXdHi%cGR@%y7Rg7?d3?an){s$zA{!H;Ie5exE#c~@NhQUFG8V=SQh%UxUeiV zd7#UcYqD=lk-}sEwlpu&H^T_V0{#G?lZMxL7ih_&{(g)MWBnCZxtXg znr#}>U^6!jA%e}@Gj49LWG@*&t0V>Cxc3?oO7LSG%~)Y5}f7vqUUnQ;STjdDU}P9IF9d9<$;=QaXc zL1^X7>fa^jHBu_}9}J~#-oz3Oq^JmGR#?GO7b9a(=R@fw@}Q{{@`Wy1vIQ#Bw?>@X z-_RGG@wt|%u`XUc%W{J z>iSeiz8C3H7@St3mOr_mU+&bL#Uif;+Xw-aZdNYUpdf>Rvu0i0t6k*}vwU`XNO2he z%miH|1tQ8~ZK!zmL&wa3E;l?!!XzgV#%PMVU!0xrDsNNZUWKlbiOjzH-1Uoxm8E#r`#2Sz;-o&qcqB zC-O_R{QGuynW14@)7&@yw1U}uP(1cov)twxeLus0s|7ayrtT8c#`&2~Fiu2=R;1_4bCaD=*E@cYI>7YSnt)nQc zohw5CsK%m?8Ack)qNx`W0_v$5S}nO|(V|RZKBD+btO?JXe|~^Qqur%@eO~<8-L^9d z=GA3-V14ng9L29~XJ>a5k~xT2152zLhM*@zlp2P5Eu}bywkcqR;ISbas&#T#;HZSf z2m69qTV(V@EkY(1Dk3`}j)JMo%ZVJ*5eB zYOjIisi+igK0#yW*gBGj?@I{~mUOvRFQR^pJbEbzFxTubnrw(Muk%}jI+vXmJ;{Q6 zrSobKD>T%}jV4Ub?L1+MGOD~0Ir%-`iTnWZN^~YPrcP5y3VMAzQ+&en^VzKEb$K!Q z<7Dbg&DNXuow*eD5yMr+#08nF!;%4vGrJI++5HdCFcGLfMW!KS*Oi@=7hFwDG!h2< zPunUEAF+HncQkbfFj&pbzp|MU*~60Z(|Ik%Tn{BXMN!hZOosNIseT?R;A`W?=d?5X zK(FB=9mZusYahp|K-wyb={rOpdn=@;4YI2W0EcbMKyo~-#^?h`BA9~o285%oY zfifCh5Lk$SY@|2A@a!T2V+{^!psQkx4?x0HSV`(w9{l75QxMk!)U52Lbhn{8ol?S) zCKo*7R(z!uk<6*qO=wh!Pul{(qq6g6xW;X68GI_CXp`XwO zxuSgPRAtM8K7}5E#-GM!*ydOOG_{A{)hkCII<|2=ma*71ci_-}VPARm3crFQjLYV! z9zbz82$|l01mv`$WahE2$=fAGWkd^X2kY(J7iz}WGS z@%MyBEO=A?HB9=^?nX`@nh;7;laAjs+fbo!|K^mE!tOB>$2a_O0y-*uaIn8k^6Y zSbuv;5~##*4Y~+y7Z5O*3w4qgI5V^17u*ZeupVGH^nM&$qmAk|anf*>r zWc5CV;-JY-Z@Uq1Irpb^O`L_7AGiqd*YpGUShb==os$uN3yYvb`wm6d=?T*it&pDk zo`vhw)RZX|91^^Wa_ti2zBFyWy4cJu#g)_S6~jT}CC{DJ_kKpT`$oAL%b^!2M;JgT zM3ZNbUB?}kP(*YYvXDIH8^7LUxz5oE%kMhF!rnPqv!GiY0o}NR$OD=ITDo9r%4E>E0Y^R(rS^~XjWyVI6 zMOR5rPXhTp*G*M&X#NTL`Hu*R+u*QNoiOKg4CtNPrjgH>c?Hi4MUG#I917fx**+pJfOo!zFM&*da&G_x)L(`k&TPI*t3e^{crd zX<4I$5nBQ8Ax_lmNRa~E*zS-R0sxkz`|>7q_?*e%7bxqNm3_eRG#1ae3gtV9!fQpY z+!^a38o4ZGy9!J5sylDxZTx$JmG!wg7;>&5H1)>f4dXj;B+@6tMlL=)cLl={jLMxY zbbf1ax3S4>bwB9-$;SN2?+GULu;UA-35;VY*^9Blx)Jwyb$=U!D>HhB&=jSsd^6yw zL)?a|>GxU!W}ocTC(?-%z3!IUhw^uzc`Vz_g>-tv)(XA#JK^)ZnC|l1`@CdX1@|!| z_9gQ)7uOf?cR@KDp97*>6X|;t@Y`k_N@)aH7gY27)COv^P3ya9I{4z~vUjLR9~z1Z z5=G{mVtKH*&$*t0@}-i_v|3B$AHHYale7>E+jP`ClqG%L{u;*ff_h@)al?RuL7tOO z->;I}>%WI{;vbLP3VIQ^iA$4wl6@0sDj|~112Y4OFjMs`13!$JGkp%b&E8QzJw_L5 zOnw9joc0^;O%OpF$Qp)W1HI!$4BaXX84`%@#^dk^hFp^pQ@rx4g(8Xjy#!X%+X5Jd@fs3amGT`}mhq#L97R>OwT5-m|h#yT_-v@(k$q7P*9X~T*3)LTdzP!*B} z+SldbVWrrwQo9wX*%FyK+sRXTa@O?WM^FGWOE?S`R(0P{<6p#f?0NJvnBia?k^fX2 zNQs7K-?EijgHJY}&zsr;qJ<*PCZUd*x|dD=IQPUK_nn)@X4KWtqoJNHkT?ZWL_hF? zS8lp2(q>;RXR|F;1O}EE#}gCrY~#n^O`_I&?&z5~7N;zL0)3Tup`%)oHMK-^r$NT% zbFg|o?b9w(q@)6w5V%si<$!U<#}s#x@0aX-hP>zwS#9*75VXA4K*%gUc>+yzupTDBOKH8WR4V0pM(HrfbQ&eJ79>HdCvE=F z|J>s;;iDLB^3(9}?biKbxf1$lI!*Z%*0&8UUq}wMyPs_hclyQQi4;NUY+x2qy|0J; zhn8;5)4ED1oHwg+VZF|80<4MrL97tGGXc5Sw$wAI#|2*cvQ=jB5+{AjMiDHmhUC*a zlmiZ`LAuAn_}hftXh;`Kq0zblDk8?O-`tnilIh|;3lZp@F_osJUV9`*R29M?7H{Fy z`nfVEIDIWXmU&YW;NjU8)EJpXhxe5t+scf|VXM!^bBlwNh)~7|3?fWwo_~ZFk(22% zTMesYw+LNx3J-_|DM~`v93yXe=jPD{q;li;5PD?Dyk+b? zo21|XpT@)$BM$%F=P9J19Vi&1#{jM3!^Y&fr&_`toi`XB1!n>sbL%U9I5<7!@?t)~ z;&H%z>bAaQ4f$wIzkjH70;<8tpUoxzKrPhn#IQfS%9l5=Iu))^XC<58D!-O z{B+o5R^Z21H0T9JQ5gNJnqh#qH^na|z92=hONIM~@_iuOi|F>jBh-?aA20}Qx~EpDGElELNn~|7WRXRFnw+Wdo`|# zBpU=Cz3z%cUJ0mx_1($X<40XEIYz(`noWeO+x#yb_pwj6)R(__%@_Cf>txOQ74wSJ z0#F3(zWWaR-jMEY$7C*3HJrohc79>MCUu26mfYN)f4M~4gD`}EX4e}A!U}QV8!S47 z6y-U-%+h`1n`*pQuKE%Av0@)+wBZr9mH}@vH@i{v(m-6QK7Ncf17x_D=)32`FOjjo zg|^VPf5c6-!FxN{25dvVh#fog=NNpXz zfB$o+0jbRkHH{!TKhE709f+jI^$3#v1Nmf80w`@7-5$1Iv_`)W^px8P-({xwb;D0y z7LKDAHgX<84?l!I*Dvi2#D@oAE^J|g$3!)x1Ua;_;<@#l1fD}lqU2_tS^6Ht$1Wl} zBESo7o^)9-Tjuz$8YQSGhfs{BQV6zW7dA?0b(Dbt=UnQs&4zHfe_sj{RJ4uS-vQpC zX;Bbsuju4%!o8?&m4UZU@~ZZjeFF6ex2ss5_60_JS_|iNc+R0GIjH1@Z z=rLT9%B|WWgOrR7IiIwr2=T;Ne?30M!@{%Qf8o`!>=s<2CBpCK_TWc(DX51>e^xh8 z&@$^b6CgOd7KXQV&Y4%}_#uN*mbanXq(2=Nj`L7H7*k(6F8s6{FOw@(DzU`4-*77{ zF+dxpv}%mFpYK?>N_2*#Y?oB*qEKB}VoQ@bzm>ptmVS_EC(#}Lxxx730trt0G)#$b zE=wVvtqOct1%*9}U{q<)2?{+0TzZzP0jgf9*)arV)*e!f`|jgT{7_9iS@e)recI#z zbzolURQ+TOzE!ymqvBY7+5NnAbWxvMLsLTwEbFqW=CPyCsmJ}P1^V30|D5E|p3BC5 z)3|qgw@ra7aXb-wsa|l^in~1_fm{7bS9jhVRkYVO#U{qMp z)Wce+|DJ}4<2gp8r0_xfZpMo#{Hl2MfjLcZdRB9(B(A(f;+4s*FxV{1F|4d`*sRNd zp4#@sEY|?^FIJ;tmH{@keZ$P(sLh5IdOk@k^0uB^BWr@pk6mHy$qf&~rI>P*a;h0C{%oA*i!VjWn&D~O#MxN&f@1Po# zKN+ zrGrkSjcr?^R#nGl<#Q722^wbYcgW@{+6CBS<1@%dPA8HC!~a`jTz<`g_l5N1M@9wn9GOAZ>nqNgq!yOCbZ@1z`U_N`Z>}+1HIZxk*5RDc&rd5{3qjRh8QmT$VyS;jK z;AF+r6XnnCp=wQYoG|rT2@8&IvKq*IB_WvS%nt%e{MCFm`&W*#LXc|HrD?nVBo=(8*=Aq?u$sDA_sC_RPDUiQ+wnIJET8vx$&fxkW~kP9qXKt zozR)@xGC!P)CTkjeWvXW5&@2?)qt)jiYWWBU?AUtzAN}{JE1I)dfz~7$;}~BmQF`k zpn11qmObXwRB8&rnEG*#4Xax3XBkKlw(;tb?Np^i+H8m(Wyz9k{~ogba@laiEk;2! zV*QV^6g6(QG%vX5Um#^sT&_e`B1pBW5yVth~xUs#0}nv?~C#l?W+9Lsb_5)!71rirGvY zTIJ$OPOY516Y|_014sNv+Z8cc5t_V=i>lWV=vNu#!58y9Zl&GsMEW#pPYPYGHQ|;vFvd*9eM==$_=vc7xnyz0~ zY}r??$<`wAO?JQk@?RGvkWVJlq2dk9vB(yV^vm{=NVI8dhsX<)O(#nr9YD?I?(VmQ z^r7VfUBn<~p3()8yOBjm$#KWx!5hRW)5Jl7wY@ky9lNM^jaT##8QGVsYeaVywmpv>X|Xj7gWE1Ezai&wVLt3p)k4w~yrskT-!PR!kiyQlaxl(( zXhF%Q9x}1TMt3~u@|#wWm-Vq?ZerK={8@~&@9r5JW}r#45#rWii};t`{5#&3$W)|@ zbAf2yDNe0q}NEUvq_Quq3cTjcw z@H_;$hu&xllCI9CFDLuScEMg|x{S7GdV8<&Mq=ezDnRZAyX-8gv97YTm0bg=d)(>N z+B2FcqvI9>jGtnK%eO%y zoBPkJTk%y`8TLf4)IXPBn`U|9>O~WL2C~C$z~9|0m*YH<-vg2CD^SX#&)B4ngOSG$ zV^wmy_iQk>dfN@Pv(ckfy&#ak@MLC7&Q6Ro#!ezM*VEh`+b3Jt%m(^T&p&WJ2Oqvj zs-4nq0TW6cv~(YI$n0UkfwN}kg3_fp?(ijSV#tR9L0}l2qjc7W?i*q01=St0eZ=4h zyGQbEw`9OEH>NMuIe)hVwYHsGERWOD;JxEiO7cQv%pFCeR+IyhwQ|y@&^24k+|8fD zLiOWFNJ2&vu2&`Jv96_z-Cd5RLgmeY3*4rDOQo?Jm`;I_(+ejsPM03!ly!*Cu}Cco zrQSrEDHNyzT(D5s1rZq!8#?f6@v6dB7a-aWs(Qk>N?UGAo{gytlh$%_IhyL7h?DLXDGx zgxGEBQoCAWo-$LRvM=F5MTle`M})t3vVv;2j0HZY&G z22^iGhV@uaJh(XyyY%} zd4iH_UfdV#T=3n}(Lj^|n;O4|$;xhu*8T3hR1mc_A}fK}jfZ7LX~*n5+`8N2q#rI$ z@<_2VANlYF$vIH$ zl<)+*tIWW78IIINA7Rr7i{<;#^yzxoLNkXL)eSs=%|P>$YQIh+ea_3k z_s7r4%j7%&*NHSl?R4k%1>Z=M9o#zxY!n8sL5>BO-ZP;T3Gut>iLS@U%IBrX6BA3k z)&@q}V8a{X<5B}K5s(c(LQ=%v1ocr`t$EqqY0EqVjr65usa=0bkf|O#ky{j3)WBR(((L^wmyHRzoWuL2~WTC=`yZ zn%VX`L=|Ok0v7?s>IHg?yArBcync5rG#^+u)>a%qjES%dRZoIyA8gQ;StH z1Ao7{<&}6U=5}4v<)1T7t!J_CL%U}CKNs-0xWoTTeqj{5{?Be$L0_tk>M9o8 zo371}S#30rKZFM{`H_(L`EM9DGp+Mifk&IP|C2Zu_)Ghr4Qtpmkm1osCf@%Z$%t+7 zYH$Cr)Ro@3-QDeQJ8m+x6%;?YYT;k6Z0E-?kr>x33`H%*ueBD7Zx~3&HtWn0?2Wt} zTG}*|v?{$ajzt}xPzV%lL1t-URi8*Zn)YljXNGDb>;!905Td|mpa@mHjIH%VIiGx- zd@MqhpYFu4_?y5N4xiHn3vX&|e6r~Xt> zZG`aGq|yTNjv;9E+Txuoa@A(9V7g?1_T5FzRI;!=NP1Kqou1z5?%X~Wwb{trRfd>i z8&y^H)8YnKyA_Fyx>}RNmQIczT?w2J4SNvI{5J&}Wto|8FR(W;Qw#b1G<1%#tmYzQ zQ2mZA-PAdi%RQOhkHy9Ea#TPSw?WxwL@H@cbkZwIq0B!@ns}niALidmn&W?!Vd4Gj zO7FiuV4*6Mr^2xlFSvM;Cp_#r8UaqIzHJQg_z^rEJw&OMm_8NGAY2)rKvki|o1bH~ z$2IbfVeY2L(^*rMRU1lM5Y_sgrDS`Z??nR2lX;zyR=c%UyGb*%TC-Dil?SihkjrQy~TMv6;BMs7P8il`H7DmpVm@rJ;b)hW)BL)GjS154b*xq-NXq2cwE z^;VP7ua2pxvCmxrnqUYQMH%a%nHmwmI33nJM(>4LznvY*k&C0{8f*%?zggpDgkuz&JBx{9mfb@wegEl2v!=}Sq2Gaty0<)UrOT0{MZtZ~j5y&w zXlYa_jY)I_+VA-^#mEox#+G>UgvM!Ac8zI<%JRXM_73Q!#i3O|)lOP*qBeJG#BST0 zqohi)O!|$|2SeJQo(w6w7%*92S})XfnhrH_Z8qe!G5>CglP=nI7JAOW?(Z29;pXJ9 zR9`KzQ=WEhy*)WH>$;7Cdz|>*i>=##0bB)oU0OR>>N<21e4rMCHDemNi2LD>Nc$;& zQRFthpWniC1J6@Zh~iJCoLOxN`oCKD5Q4r%ynwgUKPlIEd#?QViIqovY|czyK8>6B zSP%{2-<;%;1`#0mG^B(8KbtXF;Nf>K#Di72UWE4gQ%(_26Koiad)q$xRL~?pN71ZZ zujaaCx~jXjygw;rI!WB=xrOJO6HJ!!w}7eiivtCg5K|F6$EXa)=xUC za^JXSX98W`7g-tm@uo|BKj39Dl;sg5ta;4qjo^pCh~{-HdLl6qI9Ix6f$+qiZ$}s= zNguKrU;u+T@ko(Vr1>)Q%h$?UKXCY>3se%&;h2osl2D zE4A9bd7_|^njDd)6cI*FupHpE3){4NQ*$k*cOWZ_?CZ>Z4_fl@n(mMnYK62Q1d@+I zr&O))G4hMihgBqRIAJkLdk(p(D~X{-oBUA+If@B}j& zsHbeJ3RzTq96lB7d($h$xTeZ^gP0c{t!Y0c)aQE;$FY2!mACg!GDEMKXFOPI^)nHZ z`aSPJpvV0|bbrzhWWkuPURlDeN%VT8tndV8?d)eN*i4I@u zVKl^6{?}A?P)Fsy?3oi#clf}L18t;TjNI2>eI&(ezDK7RyqFxcv%>?oxUlonv(px) z$vnPzRH`y5A(x!yOIfL0bmgeMQB$H5wenx~!ujQK*nUBW;@Em&6Xv2%s(~H5WcU2R z;%Nw<$tI)a`Ve!>x+qegJnQsN2N7HaKzrFqM>`6R*gvh%O*-%THt zrB$Nk;lE;z{s{r^PPm5qz(&lM{sO*g+W{sK+m3M_z=4=&CC>T`{X}1Vg2PEfSj2x_ zmT*(x;ov%3F?qoEeeM>dUn$a*?SIGyO8m806J1W1o+4HRhc2`9$s6hM#qAm zChQ87b~GEw{ADfs+5}FJ8+|bIlIv(jT$Ap#hSHoXdd9#w<#cA<1Rkq^*EEkknUd4& zoIWIY)sAswy6fSERVm&!SO~#iN$OgOX*{9@_BWFyJTvC%S++ilSfCrO(?u=Dc?CXZ zzCG&0yVR{Z`|ZF0eEApWEo#s9osV>F{uK{QA@BES#&;#KsScf>y zvs?vIbI>VrT<*!;XmQS=bhq%46-aambZ(8KU-wOO2=en~D}MCToB_u;Yz{)1ySrPZ z@=$}EvjTdzTWU7c0ZI6L8=yP+YRD_eMMos}b5vY^S*~VZysrkq<`cK3>>v%uy7jgq z0ilW9KjVDHLv0b<1K_`1IkbTOINs0=m-22c%M~l=^S}%hbli-3?BnNq?b`hx^HX2J zIe6ECljRL0uBWb`%{EA=%!i^4sMcj+U_TaTZRb+~GOk z^ZW!nky0n*Wb*r+Q|9H@ml@Z5gU&W`(z4-j!OzC1wOke`TRAYGZVl$PmQ16{3196( zO*?`--I}Qf(2HIwb2&1FB^!faPA2=sLg(@6P4mN)>Dc3i(B0;@O-y2;lM4akD>@^v z=u>*|!s&9zem70g7zfw9FXl1bpJW(C#5w#uy5!V?Q(U35A~$dR%LDVnq@}kQm13{} zd53q3N(s$Eu{R}k2esbftfjfOITCL;jWa$}(mmm}d(&7JZ6d3%IABCapFFYjdEjdK z&4Edqf$G^MNAtL=uCDRs&Fu@FXRgX{*0<(@c3|PNHa>L%zvxWS={L8%qw`STm+=Rd zA}FLspESSIpE_^41~#5yI2bJ=9`oc;GIL!JuW&7YetZ?0H}$$%8rW@*J37L-~Rsx!)8($nI4 zZhcZ2^=Y+p4YPl%j!nFJA|*M^gc(0o$i3nlphe+~-_m}jVkRN{spFs(o0ajW@f3K{ zDV!#BwL322CET$}Y}^0ixYj2w>&Xh12|R8&yEw|wLDvF!lZ#dOTHM9pK6@Nm-@9Lnng4ZHBgBSrr7KI8YCC9DX5Kg|`HsiwJHg2(7#nS;A{b3tVO?Z% za{m5b3rFV6EpX;=;n#wltDv1LE*|g5pQ+OY&*6qCJZc5oDS6Z6JD#6F)bWxZSF@q% z+1WV;m!lRB!n^PC>RgQCI#D1br_o^#iPk>;K2hB~0^<~)?p}LG%kigm@moD#q3PE+ zA^Qca)(xnqw6x>XFhV6ku9r$E>bWNrVH9fum0?4s?Rn2LG{Vm_+QJHse6xa%nzQ?k zKug4PW~#Gtb;#5+9!QBgyB@q=sk9=$S{4T>wjFICStOM?__fr+Kei1 z3j~xPqW;W@YkiUM;HngG!;>@AITg}vAE`M2Pj9Irl4w1fo4w<|Bu!%rh%a(Ai^Zhi zs92>v5;@Y(Zi#RI*ua*h`d_7;byQSa*v9E{2x$<-_=5Z<7{%)}4XExANcz@rK69T0x3%H<@frW>RA8^swA+^a(FxK| zFl3LD*ImHN=XDUkrRhp6RY5$rQ{bRgSO*(vEHYV)3Mo6Jy3puiLmU&g82p{qr0F?ohmbz)f2r{X2|T2 z$4fdQ=>0BeKbiVM!e-lIIs8wVTuC_m7}y4A_%ikI;Wm5$9j(^Y z(cD%U%k)X>_>9~t8;pGzL6L-fmQO@K; zo&vQzMlgY95;1BSkngY)e{`n0!NfVgf}2mB3t}D9@*N;FQ{HZ3Pb%BK6;5#-O|WI( zb6h@qTLU~AbVW#_6?c!?Dj65Now7*pU{h!1+eCV^KCuPAGs28~3k@ueL5+u|Z-7}t z9|lskE`4B7W8wMs@xJa{#bsCGDFoRSNSnmNYB&U7 zVGKWe%+kFB6kb)e;TyHfqtU6~fRg)f|>=5(N36)0+C z`hv65J<$B}WUc!wFAb^QtY31yNleq4dzmG`1wHTj=c*=hay9iD071Hc?oYoUk|M*_ zU1GihAMBsM@5rUJ(qS?9ZYJ6@{bNqJ`2Mr+5#hKf?doa?F|+^IR!8lq9)wS3tF_9n zW_?hm)G(M+MYb?V9YoX^_mu5h-LP^TL^!Q9Z7|@sO(rg_4+@=PdI)WL(B7`!K^ND- z-uIuVDCVEdH_C@c71YGYT^_Scf_dhB8Z2Xy6vGtBSlYud9vggOqv^L~F{BraSE_t} zIkP+Hp2&nH^-MNEs}^`oMLy11`PQW$T|K(`Bu*(f@)mv1-qY(_YG&J2M2<7k;;RK~ zL{Fqj9yCz8(S{}@c)S!65aF<=&eLI{hAMErCx&>i7OeDN>okvegO87OaG{Jmi<|}D zaT@b|0X{d@OIJ7zvT>r+eTzgLq~|Dpu)Z&db-P4z*`M$UL51lf>FLlq6rfG)%doyp z)3kk_YIM!03eQ8Vu_2fg{+osaEJPtJ-s36R+5_AEG12`NG)IQ#TF9c@$99%0iye+ zUzZ57=m2)$D(5Nx!n)=5Au&O0BBgwxIBaeI(mro$#&UGCr<;C{UjJVAbVi%|+WP(a zL$U@TYCxJ=1{Z~}rnW;7UVb7+ZnzgmrogDxhjLGo>c~MiJAWs&&;AGg@%U?Y^0JhL ze(x6Z74JG6FlOFK(T}SXQfhr}RIFl@QXKnIcXYF)5|V~e-}suHILKT-k|<*~Ij|VF zC;t@=uj=hot~*!C68G8hTA%8SzOfETOXQ|3FSaIEjvBJp(A)7SWUi5!Eu#yWgY+;n zlm<$+UDou*V+246_o#V4kMdto8hF%%Lki#zPh}KYXmMf?hrN0;>Mv%`@{0Qn`Ujp) z=lZe+13>^Q!9zT);H<(#bIeRWz%#*}sgUX9P|9($kexOyKIOc`dLux}c$7It4u|Rl z6SSkY*V~g_B-hMPo_ak>>z@AVQ(_N)VY2kB3IZ0G(iDUYw+2d7W^~(Jq}KY=JnWS( z#rzEa&0uNhJ>QE8iiyz;n2H|SV#Og+wEZv=f2%1ELX!SX-(d3tEj$5$1}70Mp<&eI zCkfbByL7af=qQE@5vDVxx1}FSGt_a1DoE3SDI+G)mBAna)KBG4p8Epxl9QZ4BfdAN zFnF|Y(umr;gRgG6NLQ$?ZWgllEeeq~z^ZS7L?<(~O&$5|y)Al^iMKy}&W+eMm1W z7EMU)u^ke(A1#XCV>CZ71}P}0x)4wtHO8#JRG3MA-6g=`ZM!FcICCZ{IEw8Dm2&LQ z1|r)BUG^0GzI6f946RrBlfB1Vs)~8toZf~7)+G;pv&XiUO(%5bm)pl=p>nV^o*;&T z;}@oZSibzto$arQgfkp|z4Z($P>dTXE{4O=vY0!)kDO* zGF8a4wq#VaFpLfK!iELy@?-SeRrdz%F*}hjKcA*y@mj~VD3!it9lhRhX}5YOaR9$} z3mS%$2Be7{l(+MVx3 z(4?h;P!jnRmX9J9sYN#7i=iyj_5q7n#X(!cdqI2lnr8T$IfOW<_v`eB!d9xY1P=2q&WtOXY=D9QYteP)De?S4}FK6#6Ma z=E*V+#s8>L;8aVroK^6iKo=MH{4yEZ_>N-N z`(|;aOATba1^asjxlILk<4}f~`39dBFlxj>Dw(hMYKPO3EEt1@S`1lxFNM+J@uB7T zZ8WKjz7HF1-5&2=l=fqF-*@>n5J}jIxdDwpT?oKM3s8Nr`x8JnN-kCE?~aM1H!hAE z%%w(3kHfGwMnMmNj(SU(w42OrC-euI>Dsjk&jz3ts}WHqmMpzQ3vZrsXrZ|}+MHA7 z068obeXZTsO*6RS@o3x80E4ok``rV^Y3hr&C1;|ZZ0|*EKO`$lECUYG2gVFtUTw)R z4Um<0ZzlON`zTdvVdL#KFoMFQX*a5wM0Czp%wTtfK4Sjs)P**RW&?lP$(<}q%r68Z zS53Y!d@&~ne9O)A^tNrXHhXBkj~$8j%pT1%%mypa9AW5E&s9)rjF4@O3ytH{0z6riz|@< zB~UPh*wRFg2^7EbQrHf0y?E~dHlkOxof_a?M{LqQ^C!i2dawHTPYUE=X@2(3<=OOxs8qn_(y>pU>u^}3y&df{JarR0@VJn0f+U%UiF=$Wyq zQvnVHESil@d|8&R<%}uidGh7@u^(%?$#|&J$pvFC-n8&A>utA=n3#)yMkz+qnG3wd zP7xCnF|$9Dif@N~L)Vde3hW8W!UY0BgT2v(wzp;tlLmyk2%N|0jfG$%<;A&IVrOI< z!L)o>j>;dFaqA3pL}b-Je(bB@VJ4%!JeX@3x!i{yIeIso^=n?fDX`3bU=eG7sTc%g%ye8$v8P@yKE^XD=NYxTb zbf!Mk=h|otpqjFaA-vs5YOF-*GwWPc7VbaOW&stlANnCN8iftFMMrUdYNJ_Bnn5Vt zxfz@Ah|+4&P;reZxp;MmEI7C|FOv8NKUm8njF7Wb6Gi7DeODLl&G~}G4be&*Hi0Qw z5}77vL0P+7-B%UL@3n1&JPxW^d@vVwp?u#gVcJqY9#@-3X{ok#UfW3<1fb%FT`|)V~ggq z(3AUoUS-;7)^hCjdT0Kf{i}h)mBg4qhtHHBti=~h^n^OTH5U*XMgDLIR@sre`AaB$ zg)IGBET_4??m@cx&c~bA80O7B8CHR7(LX7%HThkeC*@vi{-pL%e)yXp!B2InafbDF zjPXf1mko3h59{lT6EEbxKO1Z5GF71)WwowO6kY|6tjSVSWdQ}NsK2x{>i|MKZK8%Q zfu&_0D;CO-Jg0#YmyfctyJ!mRJp)e#@O0mYdp|8x;G1%OZQ3Q847YWTyy|%^cpA;m zze0(5p{tMu^lDkpe?HynyO?a1$_LJl2L&mpeKu%8YvgRNr=%2z${%WThHG=vrWY@4 zsA`OP#O&)TetZ>s%h!=+CE15lOOls&nvC~$Qz0Ph7tHiP;O$i|eDwpT{cp>+)0-|; zY$|bB+Gbel>5aRN3>c0x)4U=|X+z+{ zn*_p*EQoquRL+=+p;=lm`d71&1NqBz&_ph)MXu(Nv6&XE7(RsS)^MGj5Q?Fwude-(sq zjJ>aOq!7!EN>@(fK7EE#;i_BGvli`5U;r!YA{JRodLBc6-`n8K+Fjgwb%sX;j=qHQ z7&Tr!)!{HXoO<2BQrV9Sw?JRaLXV8HrsNevvnf>Y-6|{T!pYLl7jp$-nEE z#X!4G4L#K0qG_4Z;Cj6=;b|Be$hi4JvMH!-voxqx^@8cXp`B??eFBz2lLD8RRaRGh zn7kUfy!YV~p(R|p7iC1Rdgt$_24i0cd-S8HpG|`@my70g^y`gu%#Tf_L21-k?sRRZHK&at(*ED0P8iw{7?R$9~OF$Ko;Iu5)ur5<->x!m93Eb zFYpIx60s=Wxxw=`$aS-O&dCO_9?b1yKiPCQmSQb>T)963`*U+Ydj5kI(B(B?HNP8r z*bfSBpSu)w(Z3j7HQoRjUG(+d=IaE~tv}y14zHHs|0UcN52fT8V_<@2ep_ee{QgZG zmgp8iv4V{k;~8@I%M3<#B;2R>Ef(Gg_cQM7%}0s*^)SK6!Ym+~P^58*wnwV1BW@eG z4sZLqsUvBbFsr#8u7S1r4teQ;t)Y@jnn_m5jS$CsW1um!p&PqAcc8!zyiXHVta9QC zY~wCwCF0U%xiQPD_INKtTb;A|Zf29(mu9NI;E zc-e>*1%(LSXB`g}kd`#}O;veb<(sk~RWL|f3ljxCnEZDdNSTDV6#Td({6l&y4IjKF z^}lIUq*ZUqgTPumD)RrCN{M^jhY>E~1pn|KOZ5((%F)G|*ZQ|r4zIbrEiV%42hJV8 z3xS)=!X1+=olbdGJ=yZil?oXLct8FM{(6ikLL3E%=q#O6(H$p~gQu6T8N!plf!96| z&Q3=`L~>U0zZh;z(pGR2^S^{#PrPxTRHD1RQOON&f)Siaf`GLj#UOk&(|@0?zm;Sx ztsGt8=29-MZs5CSf1l1jNFtNt5rFNZxJPvkNu~2}7*9468TWm>nN9TP&^!;J{-h)_ z7WsHH9|F%I`Pb!>KAS3jQWKfGivTVkMJLO-HUGM_a4UQ_%RgL6WZvrW+Z4ujZn;y@ zz9$=oO!7qVTaQAA^BhX&ZxS*|5dj803M=k&2%QrXda`-Q#IoZL6E(g+tN!6CA!CP* zCpWtCujIea)ENl0liwVfj)Nc<9mV%+e@=d`haoZ*`B7+PNjEbXBkv=B+Pi^~L#EO$D$ZqTiD8f<5$eyb54-(=3 zh)6i8i|jp(@OnRrY5B8t|LFXFQVQ895n*P16cEKTrT*~yLH6Z4e*bZ5otpRDri&+A zfNbK1D5@O=sm`fN=WzWyse!za5n%^+6dHPGX#8DyIK>?9qyX}2XvBWVqbP%%D)7$= z=#$WulZlZR<{m#gU7lwqK4WS1Ne$#_P{b17qe$~UOXCl>5b|6WVh;5vVnR<%d+Lnp z$uEmML38}U4vaW8>shm6CzB(Wei3s#NAWE3)a2)z@i{4jTn;;aQS)O@l{rUM`J@K& l00vQ5JBs~;vo!vr%%-k{2_Fq1Mn4QF81S)AQ99zk{{c4yR+0b! literal 0 HcmV?d00001 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle/wrapper/gradle-wrapper.properties b/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d4081da --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradlew b/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradlew.bat b/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/proguard-rules.pro b/packages/ffmpeg_kit_flutter_new_min_gpl/android/proguard-rules.pro new file mode 100644 index 0000000..c20e7c9 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/proguard-rules.pro @@ -0,0 +1,51 @@ +# Flutter framework and engine (core retention rules) +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } +-keep class io.flutter.view.** { *; } +-keep class io.flutter.** { *; } +-keep class io.flutter.plugins.** { *; } +-dontwarn io.flutter.embedding.** +-ignorewarnings +-keep class io.flutter.embedding.engine.plugins.** { *; } +-keep class io.flutter.embedding.engine.renderer.** { *; } +-keep class io.flutter.embedding.engine.systemchannels.** { *; } + +# FFmpegKit rules +-keep class com.antonkarpenko.ffmpegkit.** { *; } +-dontwarn com.antonkarpenko.ffmpegkit.** + +# Keep all FFmpegKit native methods +-keepclasseswithmembernames class * { + native ; +} + +# Keep FFmpegKit Config +-keep class com.antonkarpenko.ffmpegkit.FFmpegKitConfig { + *; +} + +# Keep ABI Detection +-keep class com.antonkarpenko.ffmpegkit.AbiDetect { + *; +} + +# Keep all FFmpegKit sessions +-keep class com.antonkarpenko.ffmpegkit.*Session { + *; +} + +# Keep FFmpegKit callbacks +-keep class com.antonkarpenko.ffmpegkit.*Callback { + *; +} + +# Preserve all public classes in ffmpegkit +-keep public class com.antonkarpenko.ffmpegkit.** { + public *; +} + +# Keep reflection-based access +-keepattributes *Annotation* +-keepattributes Signature +-keepattributes InnerClasses diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/settings.gradle b/packages/ffmpeg_kit_flutter_new_min_gpl/android/settings.gradle new file mode 100644 index 0000000..6bae4af --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'ffmpeg-kit-flutter-android' + diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/AndroidManifest.xml b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..20e5502 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegKitFlutterMethodResultHandler.java b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegKitFlutterMethodResultHandler.java new file mode 100644 index 0000000..85da048 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegKitFlutterMethodResultHandler.java @@ -0,0 +1,63 @@ +package com.antonkarpenko.ffmpegkit; + +import static com.antonkarpenko.ffmpegkit.FFmpegKitFlutterPlugin.LIBRARY_NAME; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel; + +public class FFmpegKitFlutterMethodResultHandler { + private final Handler handler; + + FFmpegKitFlutterMethodResultHandler() { + handler = new Handler(Looper.getMainLooper()); + } + + public void successAsync(final MethodChannel.Result result, final Object object) { + handler.post(() -> { + if (result != null) { + result.success(object); + } else { + Log.w(LIBRARY_NAME, String.format("ResultHandler can not send successful response %s on a null method call result.", object)); + } + }); + } + + void successAsync(final EventChannel.EventSink eventSink, final Object object) { + handler.post(() -> { + if (eventSink != null) { + eventSink.success(object); + } else { + Log.w(LIBRARY_NAME, String.format("ResultHandler can not send event %s on a null event sink.", object)); + } + }); + } + + void errorAsync(final MethodChannel.Result result, final String errorCode, final String errorMessage) { + errorAsync(result, errorCode, errorMessage, null); + } + + void errorAsync(final MethodChannel.Result result, final String errorCode, final String errorMessage, final Object errorDetails) { + handler.post(() -> { + if (result != null) { + result.error(errorCode, errorMessage, errorDetails); + } else { + Log.w(LIBRARY_NAME, String.format("ResultHandler can not send failure response %s:%s on a null method call result.", errorCode, errorMessage)); + } + }); + } + + void notImplementedAsync(final MethodChannel.Result result) { + handler.post(() -> { + if (result != null) { + result.notImplemented(); + } else { + Log.w(LIBRARY_NAME, "ResultHandler can not send not implemented response on a null method call result."); + } + }); + } + +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegKitFlutterPlugin.java b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegKitFlutterPlugin.java new file mode 100644 index 0000000..6a6b472 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegKitFlutterPlugin.java @@ -0,0 +1,1560 @@ +package com.antonkarpenko.ffmpegkit; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.antonkarpenko.ffmpegkit.AbiDetect; +import com.antonkarpenko.ffmpegkit.AbstractSession; +import com.antonkarpenko.ffmpegkit.FFmpegKit; +import com.antonkarpenko.ffmpegkit.FFmpegKitConfig; +import com.antonkarpenko.ffmpegkit.FFmpegSession; +import com.antonkarpenko.ffmpegkit.FFprobeKit; +import com.antonkarpenko.ffmpegkit.FFprobeSession; +import com.antonkarpenko.ffmpegkit.Level; +import com.antonkarpenko.ffmpegkit.LogRedirectionStrategy; +import com.antonkarpenko.ffmpegkit.MediaInformation; +import com.antonkarpenko.ffmpegkit.MediaInformationJsonParser; +import com.antonkarpenko.ffmpegkit.MediaInformationSession; +import com.antonkarpenko.ffmpegkit.Packages; +import com.antonkarpenko.ffmpegkit.ReturnCode; +import com.antonkarpenko.ffmpegkit.Session; +import com.antonkarpenko.ffmpegkit.SessionState; +import com.antonkarpenko.ffmpegkit.Signal; +import com.antonkarpenko.ffmpegkit.Statistics; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry; + +public class FFmpegKitFlutterPlugin implements FlutterPlugin, ActivityAware, MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.ActivityResultListener { + + public static final String LIBRARY_NAME = "ffmpeg-kit-flutter"; + public static final String PLATFORM_NAME = "android"; + + private static final String METHOD_CHANNEL = "flutter.arthenica.com/ffmpeg_kit"; + private static final String EVENT_CHANNEL = "flutter.arthenica.com/ffmpeg_kit_event"; + + // LOG CLASS + public static final String KEY_LOG_SESSION_ID = "sessionId"; + public static final String KEY_LOG_LEVEL = "level"; + public static final String KEY_LOG_MESSAGE = "message"; + + // STATISTICS CLASS + public static final String KEY_STATISTICS_SESSION_ID = "sessionId"; + public static final String KEY_STATISTICS_VIDEO_FRAME_NUMBER = "videoFrameNumber"; + public static final String KEY_STATISTICS_VIDEO_FPS = "videoFps"; + public static final String KEY_STATISTICS_VIDEO_QUALITY = "videoQuality"; + public static final String KEY_STATISTICS_SIZE = "size"; + public static final String KEY_STATISTICS_TIME = "time"; + public static final String KEY_STATISTICS_BITRATE = "bitrate"; + public static final String KEY_STATISTICS_SPEED = "speed"; + + // SESSION CLASS + public static final String KEY_SESSION_ID = "sessionId"; + public static final String KEY_SESSION_CREATE_TIME = "createTime"; + public static final String KEY_SESSION_START_TIME = "startTime"; + public static final String KEY_SESSION_COMMAND = "command"; + public static final String KEY_SESSION_TYPE = "type"; + public static final String KEY_SESSION_MEDIA_INFORMATION = "mediaInformation"; + + // SESSION TYPE + public static final int SESSION_TYPE_FFMPEG = 1; + public static final int SESSION_TYPE_FFPROBE = 2; + public static final int SESSION_TYPE_MEDIA_INFORMATION = 3; + + // EVENTS + public static final String EVENT_LOG_CALLBACK_EVENT = "FFmpegKitLogCallbackEvent"; + public static final String EVENT_STATISTICS_CALLBACK_EVENT = "FFmpegKitStatisticsCallbackEvent"; + public static final String EVENT_COMPLETE_CALLBACK_EVENT = "FFmpegKitCompleteCallbackEvent"; + + // REQUEST CODES + public static final int READABLE_REQUEST_CODE = 10000; + public static final int WRITABLE_REQUEST_CODE = 20000; + + // ARGUMENT NAMES + public static final String ARGUMENT_SESSION_ID = "sessionId"; + public static final String ARGUMENT_WAIT_TIMEOUT = "waitTimeout"; + public static final String ARGUMENT_ARGUMENTS = "arguments"; + public static final String ARGUMENT_FFPROBE_JSON_OUTPUT = "ffprobeJsonOutput"; + public static final String ARGUMENT_WRITABLE = "writable"; + + private static final int asyncConcurrencyLimit = 10; + + private final AtomicBoolean logsEnabled; + private final AtomicBoolean statisticsEnabled; + private final ExecutorService asyncExecutorService; + + private MethodChannel methodChannel; + private EventChannel eventChannel; + private Result lastInitiatedIntentResult; + private Context context; + private Activity activity; + private FlutterPluginBinding flutterPluginBinding; + private ActivityPluginBinding activityPluginBinding; + + private EventChannel.EventSink eventSink; + private final FFmpegKitFlutterMethodResultHandler resultHandler; + + // FFmpegKit's native callbacks are global (last registration wins), so they are + // registered exactly once and dispatch to every attached plugin instance that has + // a live event sink. Registering them per-instance let the most recently attached + // engine (e.g. a Firebase Messaging background isolate that never listens) capture + // all events and drop them on its null sink (#163). + private static final java.util.Set attachedPlugins = + java.util.concurrent.ConcurrentHashMap.newKeySet(); + private static final AtomicBoolean globalCallbacksRegistered = new AtomicBoolean(false); + + public FFmpegKitFlutterPlugin() { + this.logsEnabled = new AtomicBoolean(false); + this.statisticsEnabled = new AtomicBoolean(false); + this.asyncExecutorService = Executors.newFixedThreadPool(asyncConcurrencyLimit); + this.resultHandler = new FFmpegKitFlutterMethodResultHandler(); + + Log.d(LIBRARY_NAME, String.format("FFmpegKitFlutterPlugin created %s.", this)); + } + + protected static void registerGlobalCallbacks() { + if (!globalCallbacksRegistered.compareAndSet(false, true)) { + return; + } + + FFmpegKitConfig.enableFFmpegSessionCompleteCallback(FFmpegKitFlutterPlugin::broadcastSession); + FFmpegKitConfig.enableFFprobeSessionCompleteCallback(FFmpegKitFlutterPlugin::broadcastSession); + FFmpegKitConfig.enableMediaInformationSessionCompleteCallback(FFmpegKitFlutterPlugin::broadcastSession); + + FFmpegKitConfig.enableLogCallback(log -> { + for (FFmpegKitFlutterPlugin plugin : attachedPlugins) { + if (plugin.logsEnabled.get() && plugin.eventSink != null) { + plugin.emitLog(log); + } + } + }); + + FFmpegKitConfig.enableStatisticsCallback(statistics -> { + for (FFmpegKitFlutterPlugin plugin : attachedPlugins) { + if (plugin.statisticsEnabled.get() && plugin.eventSink != null) { + plugin.emitStatistics(statistics); + } + } + }); + } + + private static void broadcastSession(final Session session) { + boolean delivered = false; + for (FFmpegKitFlutterPlugin plugin : attachedPlugins) { + if (plugin.eventSink != null) { + plugin.emitSession(session); + delivered = true; + } + } + if (!delivered) { + Log.w(LIBRARY_NAME, String.format("No attached engine is listening; complete event for session %d was not delivered.", session.getSessionId())); + } + } + + @Override + public void onAttachedToEngine(@NonNull final FlutterPluginBinding flutterPluginBinding) { + this.flutterPluginBinding = flutterPluginBinding; + attachedPlugins.add(this); + // Both channels must be registered here, not in onAttachedToActivity: engines without + // an Activity (e.g. background service isolates) never attach to one, and the event + // channel would be left without a stream handler (#155). + init(flutterPluginBinding.getBinaryMessenger(), flutterPluginBinding.getApplicationContext()); + } + + @Override + public void onDetachedFromEngine(@NonNull final FlutterPluginBinding binding) { + attachedPlugins.remove(this); + uninit(); + this.flutterPluginBinding = null; + } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { + this.activity = activityPluginBinding.getActivity(); + this.activityPluginBinding = activityPluginBinding; + activityPluginBinding.addActivityResultListener(this); + Log.d(LIBRARY_NAME, String.format("FFmpegKitFlutterPlugin %s attached to activity %s.", this, activity)); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity(); + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding activityPluginBinding) { + onAttachedToActivity(activityPluginBinding); + } + + @Override + public void onDetachedFromActivity() { + if (activityPluginBinding != null) { + activityPluginBinding.removeActivityResultListener(this); + } + this.activity = null; + this.activityPluginBinding = null; + Log.d(LIBRARY_NAME, "FFmpegKitFlutterPlugin detached from activity."); + } + + protected void init(final BinaryMessenger messenger, final Context context) { + registerGlobalCallbacks(); + + if (methodChannel == null) { + methodChannel = new MethodChannel(messenger, METHOD_CHANNEL); + methodChannel.setMethodCallHandler(this); + } else { + Log.i(LIBRARY_NAME, "FFmpegKitFlutterPlugin method channel was already initialised."); + } + + if (eventChannel == null) { + eventChannel = new EventChannel(messenger, EVENT_CHANNEL); + eventChannel.setStreamHandler(this); + } else { + Log.i(LIBRARY_NAME, "FFmpegKitFlutterPlugin event channel was already initialised."); + } + + this.context = context; + + Log.d(LIBRARY_NAME, String.format("FFmpegKitFlutterPlugin %s initialised with context %s.", this, context)); + } + + @Override + public void onListen(final Object o, final EventChannel.EventSink eventSink) { + this.eventSink = eventSink; +// Log.d(LIBRARY_NAME, String.format("FFmpegKitFlutterPlugin %s started listening to events on %s.", this, eventSink)); + } + + @Override + public void onCancel(final Object o) { + this.eventSink = null; + Log.d(LIBRARY_NAME, "FFmpegKitFlutterPlugin stopped listening to events."); + } + + @Override + public boolean onActivityResult(final int requestCode, final int resultCode, final Intent data) { + Log.d(LIBRARY_NAME, String.format("selectDocument completed with requestCode: %d, resultCode: %d, data: %s.", requestCode, resultCode, data == null ? null : data.toString())); + + if (requestCode == READABLE_REQUEST_CODE || requestCode == WRITABLE_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + if (data == null) { + resultHandler.successAsync(lastInitiatedIntentResult, null); + } else { + final Uri uri = data.getData(); + resultHandler.successAsync(lastInitiatedIntentResult, uri == null ? null : uri.toString()); + } + } else { + resultHandler.errorAsync(lastInitiatedIntentResult, "SELECT_CANCELLED", String.valueOf(resultCode)); + } + + return true; + } else { + Log.i(LIBRARY_NAME, String.format("FFmpegKitFlutterPlugin ignored unsupported activity result for requestCode: %d.", requestCode)); + return false; + } + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + final Integer sessionId = call.argument(ARGUMENT_SESSION_ID); + final Integer waitTimeout = call.argument(ARGUMENT_WAIT_TIMEOUT); + final List arguments = call.argument(ARGUMENT_ARGUMENTS); + final String ffprobeJsonOutput = call.argument(ARGUMENT_FFPROBE_JSON_OUTPUT); + final Boolean writable = call.argument(ARGUMENT_WRITABLE); + + switch (call.method) { + case "abstractSessionGetEndTime": + if (sessionId != null) { + abstractSessionGetEndTime(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "abstractSessionGetDuration": + if (sessionId != null) { + abstractSessionGetDuration(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "abstractSessionGetAllLogs": + if (sessionId != null) { + abstractSessionGetAllLogs(sessionId, waitTimeout, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "abstractSessionGetLogs": + if (sessionId != null) { + abstractSessionGetLogs(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "abstractSessionGetAllLogsAsString": + if (sessionId != null) { + abstractSessionGetAllLogsAsString(sessionId, waitTimeout, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "abstractSessionGetState": + if (sessionId != null) { + abstractSessionGetState(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "abstractSessionGetReturnCode": + if (sessionId != null) { + abstractSessionGetReturnCode(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "abstractSessionGetFailStackTrace": + if (sessionId != null) { + abstractSessionGetFailStackTrace(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "thereAreAsynchronousMessagesInTransmit": + if (sessionId != null) { + abstractSessionThereAreAsynchronousMessagesInTransmit(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "getArch": + getArch(result); + break; + case "ffmpegSession": + if (arguments != null) { + ffmpegSession(arguments, result); + } else { + resultHandler.errorAsync(result, "INVALID_ARGUMENTS", "Invalid arguments array."); + } + break; + case "ffmpegSessionGetAllStatistics": + if (sessionId != null) { + ffmpegSessionGetAllStatistics(sessionId, waitTimeout, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "ffmpegSessionGetStatistics": + if (sessionId != null) { + ffmpegSessionGetStatistics(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "ffprobeSession": + if (arguments != null) { + ffprobeSession(arguments, result); + } else { + resultHandler.errorAsync(result, "INVALID_ARGUMENTS", "Invalid arguments array."); + } + break; + case "mediaInformationSession": + if (arguments != null) { + mediaInformationSession(arguments, result); + } else { + resultHandler.errorAsync(result, "INVALID_ARGUMENTS", "Invalid arguments array."); + } + break; + case "getMediaInformation": + if (sessionId != null) { + getMediaInformation(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "mediaInformationJsonParserFrom": + if (ffprobeJsonOutput != null) { + mediaInformationJsonParserFrom(ffprobeJsonOutput, result); + } else { + resultHandler.errorAsync(result, "INVALID_FFPROBE_JSON_OUTPUT", "Invalid ffprobe json output."); + } + break; + case "mediaInformationJsonParserFromWithError": + if (ffprobeJsonOutput != null) { + mediaInformationJsonParserFromWithError(ffprobeJsonOutput, result); + } else { + resultHandler.errorAsync(result, "INVALID_FFPROBE_JSON_OUTPUT", "Invalid ffprobe json output."); + } + break; + case "enableRedirection": + enableRedirection(result); + break; + case "disableRedirection": + disableRedirection(result); + break; + case "enableLogs": + enableLogs(result); + break; + case "disableLogs": + disableLogs(result); + break; + case "enableStatistics": + enableStatistics(result); + break; + case "disableStatistics": + disableStatistics(result); + break; + case "setFontconfigConfigurationPath": + final String path = call.argument("path"); + if (path != null) { + setFontconfigConfigurationPath(path, result); + } else { + resultHandler.errorAsync(result, "INVALID_PATH", "Invalid path."); + } + break; + case "setFontDirectory": { + final String fontDirectory = call.argument("fontDirectory"); + final Map fontNameMap = call.argument("fontNameMap"); + if (fontDirectory != null) { + setFontDirectory(fontDirectory, fontNameMap, result); + } else { + resultHandler.errorAsync(result, "INVALID_FONT_DIRECTORY", "Invalid font directory."); + } + break; + } + case "setFontDirectoryList": { + final List fontDirectoryList = call.argument("fontDirectoryList"); + final Map fontNameMap = call.argument("fontNameMap"); + if (fontDirectoryList != null) { + setFontDirectoryList(fontDirectoryList, fontNameMap, result); + } else { + resultHandler.errorAsync(result, "INVALID_FONT_DIRECTORY_LIST", "Invalid font directory list."); + } + break; + } + case "registerNewFFmpegPipe": + registerNewFFmpegPipe(result); + break; + case "closeFFmpegPipe": + final String ffmpegPipePath = call.argument("ffmpegPipePath"); + if (ffmpegPipePath != null) { + closeFFmpegPipe(ffmpegPipePath, result); + } else { + resultHandler.errorAsync(result, "INVALID_PIPE_PATH", "Invalid ffmpeg pipe path."); + } + break; + case "getFFmpegVersion": + getFFmpegVersion(result); + break; + case "isLTSBuild": + isLTSBuild(result); + break; + case "getBuildDate": + getBuildDate(result); + break; + case "setEnvironmentVariable": + final String variableName = call.argument("variableName"); + final String variableValue = call.argument("variableValue"); + + if (variableName != null && variableValue != null) { + setEnvironmentVariable(variableName, variableValue, result); + } else if (variableValue != null) { + resultHandler.errorAsync(result, "INVALID_NAME", "Invalid environment variable name."); + } else { + resultHandler.errorAsync(result, "INVALID_VALUE", "Invalid environment variable value."); + } + break; + case "ignoreSignal": + final Integer signal = call.argument("signal"); + if (signal != null) { + ignoreSignal(signal, result); + } else { + resultHandler.errorAsync(result, "INVALID_SIGNAL", "Invalid signal value."); + } + break; + case "ffmpegSessionExecute": + if (sessionId != null) { + ffmpegSessionExecute(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "ffprobeSessionExecute": + if (sessionId != null) { + ffprobeSessionExecute(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "mediaInformationSessionExecute": + if (sessionId != null) { + mediaInformationSessionExecute(sessionId, waitTimeout, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "asyncFFmpegSessionExecute": + if (sessionId != null) { + asyncFFmpegSessionExecute(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "asyncFFprobeSessionExecute": + if (sessionId != null) { + asyncFFprobeSessionExecute(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "asyncMediaInformationSessionExecute": + if (sessionId != null) { + asyncMediaInformationSessionExecute(sessionId, waitTimeout, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "getLogLevel": + getLogLevel(result); + break; + case "setLogLevel": + final Integer level = call.argument("level"); + if (level != null) { + setLogLevel(level, result); + } else { + resultHandler.errorAsync(result, "INVALID_LEVEL", "Invalid level value."); + } + break; + case "getSessionHistorySize": + getSessionHistorySize(result); + break; + case "setSessionHistorySize": + final Integer sessionHistorySize = call.argument("sessionHistorySize"); + if (sessionHistorySize != null) { + setSessionHistorySize(sessionHistorySize, result); + } else { + resultHandler.errorAsync(result, "INVALID_SIZE", "Invalid session history size value."); + } + break; + case "getSession": + if (sessionId != null) { + getSession(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "getLastSession": + getLastSession(result); + break; + case "getLastCompletedSession": + getLastCompletedSession(result); + break; + case "getSessions": + getSessions(result); + break; + case "clearSessions": + clearSessions(result); + break; + case "getSessionsByState": + final Integer state = call.argument("state"); + if (state != null) { + getSessionsByState(state, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION_STATE", "Invalid session state value."); + } + break; + case "getLogRedirectionStrategy": + getLogRedirectionStrategy(result); + break; + case "setLogRedirectionStrategy": + final Integer strategy = call.argument("strategy"); + if (strategy != null) { + setLogRedirectionStrategy(strategy, result); + } else { + resultHandler.errorAsync(result, "INVALID_LOG_REDIRECTION_STRATEGY", "Invalid log redirection strategy value."); + } + break; + case "messagesInTransmit": + if (sessionId != null) { + messagesInTransmit(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "getPlatform": + getPlatform(result); + break; + case "writeToPipe": + final String input = call.argument("input"); + final String pipe = call.argument("pipe"); + if (input != null && pipe != null) { + writeToPipe(input, pipe, result); + } else if (pipe != null) { + resultHandler.errorAsync(result, "INVALID_INPUT", "Invalid input value."); + } else { + resultHandler.errorAsync(result, "INVALID_PIPE", "Invalid pipe value."); + } + break; + case "selectDocument": + final String title = call.argument("title"); + final String type = call.argument("type"); + final List extraTypes = call.argument("extraTypes"); + final String[] extraTypesArray; + if (extraTypes != null) { + extraTypesArray = extraTypes.toArray(new String[0]); + } else { + extraTypesArray = null; + } + if (writable != null) { + selectDocument(writable, title, type, extraTypesArray, result); + } else { + resultHandler.errorAsync(result, "INVALID_WRITABLE", "Invalid writable value."); + } + break; + case "getSafParameter": + final String uri = call.argument("uri"); + final String openMode = call.argument("openMode"); + if (uri != null && openMode != null) { + getSafParameter(uri, openMode, result); + } else if (uri != null) { + resultHandler.errorAsync(result, "INVALID_OPEN_MODE", "Invalid openMode value."); + } else { + resultHandler.errorAsync(result, "INVALID_URI", "Invalid uri value."); + } + break; + case "cancel": + cancel(result); + break; + case "cancelSession": + if (sessionId != null) { + cancelSession(sessionId, result); + } else { + resultHandler.errorAsync(result, "INVALID_SESSION", "Invalid session id."); + } + break; + case "getFFmpegSessions": + getFFmpegSessions(result); + break; + case "getFFprobeSessions": + getFFprobeSessions(result); + break; + case "getMediaInformationSessions": + getMediaInformationSessions(result); + break; + case "getPackageName": + getPackageName(result); + break; + case "getExternalLibraries": + getExternalLibraries(result); + break; + default: + resultHandler.notImplementedAsync(result); + break; + } + } + + protected void uninit() { + uninitMethodChannel(); + uninitEventChannel(); + + this.context = null; + + Log.d(LIBRARY_NAME, "FFmpegKitFlutterPlugin uninitialized."); + } + + protected void uninitMethodChannel() { + if (methodChannel == null) { + Log.i(LIBRARY_NAME, "FFmpegKitFlutterPlugin method channel was already uninitialised."); + return; + } + + methodChannel.setMethodCallHandler(null); + methodChannel = null; + } + + protected void uninitEventChannel() { + if (eventChannel == null) { + Log.i(LIBRARY_NAME, "FFmpegKitFlutterPlugin event channel was already uninitialised."); + return; + } + + eventChannel.setStreamHandler(null); + eventChannel = null; + } + + // AbstractSession + + protected void abstractSessionGetEndTime(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + final Date endTime = session.getEndTime(); + if (endTime == null) { + resultHandler.successAsync(result, null); + } else { + resultHandler.successAsync(result, endTime.getTime()); + } + } + } + + protected void abstractSessionGetDuration(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + resultHandler.successAsync(result, session.getDuration()); + } + } + + protected void abstractSessionGetAllLogs(@NonNull final Integer sessionId, @Nullable final Integer waitTimeout, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + final int timeout; + if (isValidPositiveNumber(waitTimeout)) { + timeout = waitTimeout; + } else { + timeout = AbstractSession.DEFAULT_TIMEOUT_FOR_ASYNCHRONOUS_MESSAGES_IN_TRANSMIT; + } + final List allLogs = session.getAllLogs(timeout); + resultHandler.successAsync(result, toLogMapList(allLogs)); + } + } + + protected void abstractSessionGetLogs(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + final List allLogs = session.getLogs(); + resultHandler.successAsync(result, toLogMapList(allLogs)); + } + } + + protected void abstractSessionGetAllLogsAsString(@NonNull final Integer sessionId, @Nullable final Integer waitTimeout, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + final int timeout; + if (isValidPositiveNumber(waitTimeout)) { + timeout = waitTimeout; + } else { + timeout = AbstractSession.DEFAULT_TIMEOUT_FOR_ASYNCHRONOUS_MESSAGES_IN_TRANSMIT; + } + final String allLogsAsString = session.getAllLogsAsString(timeout); + resultHandler.successAsync(result, allLogsAsString); + } + } + + protected void abstractSessionGetState(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + resultHandler.successAsync(result, session.getState().ordinal()); + } + } + + protected void abstractSessionGetReturnCode(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + final ReturnCode returnCode = session.getReturnCode(); + if (returnCode == null) { + resultHandler.successAsync(result, null); + } else { + resultHandler.successAsync(result, returnCode.getValue()); + } + } + } + + protected void abstractSessionGetFailStackTrace(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + resultHandler.successAsync(result, session.getFailStackTrace()); + } + } + + protected void abstractSessionThereAreAsynchronousMessagesInTransmit(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + resultHandler.successAsync(result, session.thereAreAsynchronousMessagesInTransmit()); + } + } + + // ArchDetect + + protected void getArch(@NonNull final Result result) { + resultHandler.successAsync(result, AbiDetect.getAbi()); + } + + // FFmpegSession + + protected void ffmpegSession(@NonNull final List arguments, @NonNull final Result result) { + final FFmpegSession session = FFmpegSession.create(arguments.toArray(new String[0]), null, null, null, LogRedirectionStrategy.NEVER_PRINT_LOGS); + resultHandler.successAsync(result, toMap(session)); + } + + protected void ffmpegSessionGetAllStatistics(@NonNull final Integer sessionId, @Nullable final Integer waitTimeout, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + if (session.isFFmpeg()) { + final int timeout; + if (isValidPositiveNumber(waitTimeout)) { + timeout = waitTimeout; + } else { + timeout = AbstractSession.DEFAULT_TIMEOUT_FOR_ASYNCHRONOUS_MESSAGES_IN_TRANSMIT; + } + final List allStatistics = ((FFmpegSession) session).getAllStatistics(timeout); + resultHandler.successAsync(result, toStatisticsMapList(allStatistics)); + } else { + resultHandler.errorAsync(result, "NOT_FFMPEG_SESSION", "A session is found but it does not have the correct type."); + } + } + } + + protected void ffmpegSessionGetStatistics(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + if (session.isFFmpeg()) { + final List statistics = ((FFmpegSession) session).getStatistics(); + resultHandler.successAsync(result, toStatisticsMapList(statistics)); + } else { + resultHandler.errorAsync(result, "NOT_FFMPEG_SESSION", "A session is found but it does not have the correct type."); + } + } + } + + // FFprobeSession + + protected void ffprobeSession(@NonNull final List arguments, @NonNull final Result result) { + final FFprobeSession session = FFprobeSession.create(arguments.toArray(new String[0]), null, null, LogRedirectionStrategy.NEVER_PRINT_LOGS); + resultHandler.successAsync(result, toMap(session)); + } + + // MediaInformationSession + + protected void mediaInformationSession(@NonNull final List arguments, @NonNull final Result result) { + final MediaInformationSession session = MediaInformationSession.create(arguments.toArray(new String[0]), null, null); + resultHandler.successAsync(result, toMap(session)); + } + + protected void getMediaInformation(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + if (session.isMediaInformation()) { + final MediaInformationSession mediaInformationSession = (MediaInformationSession) session; + final MediaInformation mediaInformation = mediaInformationSession.getMediaInformation(); + resultHandler.successAsync(result, toMap(mediaInformation)); + } else { + resultHandler.errorAsync(result, "NOT_MEDIA_INFORMATION_SESSION", "A session is found but it does not have the correct type."); + } + } + } + + // MediaInformationJsonParser + + protected void mediaInformationJsonParserFrom(@NonNull final String ffprobeJsonOutput, @NonNull final Result result) { + try { + final MediaInformation mediaInformation = MediaInformationJsonParser.fromWithError(ffprobeJsonOutput); + resultHandler.successAsync(result, toMap(mediaInformation)); + } catch (final JSONException e) { + Log.i(LIBRARY_NAME, "Parsing MediaInformation failed.", e); + resultHandler.successAsync(result, null); + } + } + + protected void mediaInformationJsonParserFromWithError(@NonNull final String ffprobeJsonOutput, @NonNull final Result result) { + try { + final MediaInformation mediaInformation = MediaInformationJsonParser.fromWithError(ffprobeJsonOutput); + resultHandler.successAsync(result, toMap(mediaInformation)); + } catch (JSONException e) { + Log.i(LIBRARY_NAME, "Parsing MediaInformation failed.", e); + resultHandler.errorAsync(result, "PARSE_FAILED", "Parsing MediaInformation failed with JSON error."); + } + } + + // FFmpegKitConfig + + protected void enableRedirection(@NonNull final Result result) { + enableLogs(); + enableStatistics(); + FFmpegKitConfig.enableRedirection(); + + resultHandler.successAsync(result, null); + } + + protected void disableRedirection(@NonNull final Result result) { + FFmpegKitConfig.disableRedirection(); + + resultHandler.successAsync(result, null); + } + + protected void enableLogs(@NonNull final Result result) { + enableLogs(); + + resultHandler.successAsync(result, null); + } + + protected void disableLogs(@NonNull final Result result) { + disableLogs(); + + resultHandler.successAsync(result, null); + } + + protected void enableStatistics(@NonNull final Result result) { + enableStatistics(); + + resultHandler.successAsync(result, null); + } + + protected void disableStatistics(@NonNull final Result result) { + disableStatistics(); + + resultHandler.successAsync(result, null); + } + + protected void setFontconfigConfigurationPath(@NonNull final String path, @NonNull final Result result) { + FFmpegKitConfig.setFontconfigConfigurationPath(path); + + resultHandler.successAsync(result, null); + } + + protected void setFontDirectory(@NonNull final String fontDirectoryPath, @Nullable final Map fontNameMapping, @NonNull final Result result) { + if (context != null) { + FFmpegKitConfig.setFontDirectory(context, fontDirectoryPath, fontNameMapping); + resultHandler.successAsync(result, null); + } else { + Log.w(LIBRARY_NAME, "Cannot setFontDirectory. Context is null."); + resultHandler.errorAsync(result, "INVALID_CONTEXT", "Context is null."); + } + } + + protected void setFontDirectoryList(@NonNull final List fontDirectoryList, @Nullable final Map fontNameMapping, @NonNull final Result result) { + if (context != null) { + FFmpegKitConfig.setFontDirectoryList(context, fontDirectoryList, fontNameMapping); + resultHandler.successAsync(result, null); + } else { + Log.w(LIBRARY_NAME, "Cannot setFontDirectoryList. Context is null."); + resultHandler.errorAsync(result, "INVALID_CONTEXT", "Context is null."); + } + } + + protected void registerNewFFmpegPipe(@NonNull final Result result) { + if (context != null) { + resultHandler.successAsync(result, FFmpegKitConfig.registerNewFFmpegPipe(context)); + } else { + Log.w(LIBRARY_NAME, "Cannot registerNewFFmpegPipe. Context is null."); + resultHandler.errorAsync(result, "INVALID_CONTEXT", "Context is null."); + } + } + + protected void closeFFmpegPipe(@NonNull final String ffmpegPipePath, @NonNull final Result result) { + FFmpegKitConfig.closeFFmpegPipe(ffmpegPipePath); + + resultHandler.successAsync(result, null); + } + + protected void getFFmpegVersion(@NonNull final Result result) { + resultHandler.successAsync(result, FFmpegKitConfig.getFFmpegVersion()); + } + + protected void isLTSBuild(@NonNull final Result result) { + resultHandler.successAsync(result, FFmpegKitConfig.isLTSBuild()); + } + + protected void getBuildDate(@NonNull final Result result) { + resultHandler.successAsync(result, FFmpegKitConfig.getBuildDate()); + } + + protected void setEnvironmentVariable(@NonNull final String variableName, @NonNull final String variableValue, @NonNull final Result result) { + FFmpegKitConfig.setEnvironmentVariable(variableName, variableValue); + + resultHandler.successAsync(result, null); + } + + protected void ignoreSignal(@NonNull final Integer signalIndex, @NonNull final Result result) { + Signal signal = null; + + if (signalIndex == Signal.SIGINT.ordinal()) { + signal = Signal.SIGINT; + } else if (signalIndex == Signal.SIGQUIT.ordinal()) { + signal = Signal.SIGQUIT; + } else if (signalIndex == Signal.SIGPIPE.ordinal()) { + signal = Signal.SIGPIPE; + } else if (signalIndex == Signal.SIGTERM.ordinal()) { + signal = Signal.SIGTERM; + } else if (signalIndex == Signal.SIGXCPU.ordinal()) { + signal = Signal.SIGXCPU; + } + + if (signal != null) { + FFmpegKitConfig.ignoreSignal(signal); + + resultHandler.successAsync(result, null); + } else { + resultHandler.errorAsync(result, "INVALID_SIGNAL", "Signal value not supported."); + } + } + + protected void ffmpegSessionExecute(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + if (session.isFFmpeg()) { + final FFmpegSessionExecuteTask ffmpegSessionExecuteTask = new FFmpegSessionExecuteTask((FFmpegSession) session, resultHandler, result); + asyncExecutorService.submit(ffmpegSessionExecuteTask); + } else { + resultHandler.errorAsync(result, "NOT_FFMPEG_SESSION", "A session is found but it does not have the correct type."); + } + } + } + + protected void ffprobeSessionExecute(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + if (session.isFFprobe()) { + final FFprobeSessionExecuteTask ffprobeSessionExecuteTask = new FFprobeSessionExecuteTask((FFprobeSession) session, resultHandler, result); + asyncExecutorService.submit(ffprobeSessionExecuteTask); + } else { + resultHandler.errorAsync(result, "NOT_FFPROBE_SESSION", "A session is found but it does not have the correct type."); + } + } + } + + protected void mediaInformationSessionExecute(@NonNull final Integer sessionId, @Nullable final Integer waitTimeout, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + if (session.isMediaInformation()) { + final int timeout; + if (isValidPositiveNumber(waitTimeout)) { + timeout = waitTimeout; + } else { + timeout = AbstractSession.DEFAULT_TIMEOUT_FOR_ASYNCHRONOUS_MESSAGES_IN_TRANSMIT; + } + final MediaInformationSessionExecuteTask mediaInformationSessionExecuteTask = new MediaInformationSessionExecuteTask((MediaInformationSession) session, timeout, resultHandler, result); + asyncExecutorService.submit(mediaInformationSessionExecuteTask); + } else { + resultHandler.errorAsync(result, "NOT_MEDIA_INFORMATION_SESSION", "A session is found but it does not have the correct type."); + } + } + } + + protected void asyncFFmpegSessionExecute(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + if (session.isFFmpeg()) { + FFmpegKitConfig.asyncFFmpegExecute((FFmpegSession) session); + resultHandler.successAsync(result, null); + } else { + resultHandler.errorAsync(result, "NOT_FFMPEG_SESSION", "A session is found but it does not have the correct type."); + } + } + } + + protected void asyncFFprobeSessionExecute(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + if (session.isFFprobe()) { + FFmpegKitConfig.asyncFFprobeExecute((FFprobeSession) session); + resultHandler.successAsync(result, null); + } else { + resultHandler.errorAsync(result, "NOT_FFPROBE_SESSION", "A session is found but it does not have the correct type."); + } + } + } + + protected void asyncMediaInformationSessionExecute(@NonNull final Integer sessionId, @Nullable final Integer waitTimeout, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + if (session.isMediaInformation()) { + final int timeout; + if (isValidPositiveNumber(waitTimeout)) { + timeout = waitTimeout; + } else { + timeout = AbstractSession.DEFAULT_TIMEOUT_FOR_ASYNCHRONOUS_MESSAGES_IN_TRANSMIT; + } + FFmpegKitConfig.asyncGetMediaInformationExecute((MediaInformationSession) session, timeout); + resultHandler.successAsync(result, null); + } else { + resultHandler.errorAsync(result, "NOT_MEDIA_INFORMATION_SESSION", "A session is found but it does not have the correct type."); + } + } + } + + protected void getLogLevel(@NonNull final Result result) { + resultHandler.successAsync(result, toInt(FFmpegKitConfig.getLogLevel())); + } + + protected void setLogLevel(@NonNull final Integer level, @NonNull final Result result) { + FFmpegKitConfig.setLogLevel(Level.from(level)); + resultHandler.successAsync(result, null); + } + + protected void getSessionHistorySize(@NonNull final Result result) { + resultHandler.successAsync(result, FFmpegKitConfig.getSessionHistorySize()); + } + + protected void setSessionHistorySize(@NonNull final Integer sessionHistorySize, @NonNull final Result result) { + FFmpegKitConfig.setSessionHistorySize(sessionHistorySize); + resultHandler.successAsync(result, null); + } + + protected void getSession(@NonNull final Integer sessionId, @NonNull final Result result) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + resultHandler.errorAsync(result, "SESSION_NOT_FOUND", "Session not found."); + } else { + resultHandler.successAsync(result, toMap(session)); + } + } + + protected void getLastSession(@NonNull final Result result) { + final Session session = FFmpegKitConfig.getLastSession(); + resultHandler.successAsync(result, toMap(session)); + } + + protected void getLastCompletedSession(@NonNull final Result result) { + final Session session = FFmpegKitConfig.getLastCompletedSession(); + resultHandler.successAsync(result, toMap(session)); + } + + protected void getSessions(@NonNull final Result result) { + resultHandler.successAsync(result, toSessionMapList(FFmpegKitConfig.getSessions())); + } + + protected void clearSessions(@NonNull final Result result) { + FFmpegKitConfig.clearSessions(); + resultHandler.successAsync(result, null); + } + + protected void getSessionsByState(@NonNull final Integer sessionState, @NonNull final Result result) { + resultHandler.successAsync(result, toSessionMapList(FFmpegKitConfig.getSessionsByState(toSessionState(sessionState)))); + } + + protected void getLogRedirectionStrategy(@NonNull final Result result) { + resultHandler.successAsync(result, toInt(FFmpegKitConfig.getLogRedirectionStrategy())); + } + + protected void setLogRedirectionStrategy(@NonNull final Integer logRedirectionStrategy, @NonNull final Result result) { + FFmpegKitConfig.setLogRedirectionStrategy(toLogRedirectionStrategy(logRedirectionStrategy)); + resultHandler.successAsync(result, null); + } + + protected void messagesInTransmit(@NonNull final Integer sessionId, @NonNull final Result result) { + resultHandler.successAsync(result, FFmpegKitConfig.messagesInTransmit(sessionId.longValue())); + } + + protected void getPlatform(@NonNull final Result result) { + resultHandler.successAsync(result, PLATFORM_NAME); + } + + protected void writeToPipe(@NonNull final String inputPath, @NonNull final String namedPipePath, @NonNull final Result result) { + final WriteToPipeTask asyncTask = new WriteToPipeTask(inputPath, namedPipePath, resultHandler, result); + asyncExecutorService.submit(asyncTask); + } + + protected void selectDocument(@NonNull final Boolean writable, @Nullable final String title, @Nullable final String type, @Nullable final String[] extraTypes, @NonNull final Result result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + Log.i(LIBRARY_NAME, String.format(Locale.getDefault(), "selectDocument is not supported on API Level %d", Build.VERSION.SDK_INT)); + resultHandler.errorAsync(result, "SELECT_FAILED", String.format(Locale.getDefault(), "selectDocument is not supported on API Level %d", Build.VERSION.SDK_INT)); + return; + } + + final Intent intent; + if (writable) { + intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } else { + intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + if (type != null) { + intent.setType(type); + } else { + intent.setType("*/*"); + } + + if (title != null) { + intent.putExtra(Intent.EXTRA_TITLE, title); + } + + if (extraTypes != null) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, extraTypes); + } + + if (context != null) { + if (activity != null) { + try { + lastInitiatedIntentResult = result; + activity.startActivityForResult(intent, writable ? WRITABLE_REQUEST_CODE : READABLE_REQUEST_CODE); + } catch (final Exception e) { + Log.i(LIBRARY_NAME, String.format("Failed to selectDocument using parameters writable: %s, type: %s, title: %s and extra types: %s!", writable, type, title, extraTypes == null ? null : Arrays.toString(extraTypes)), e); + resultHandler.errorAsync(result, "SELECT_FAILED", e.getMessage()); + } + } else { + Log.w(LIBRARY_NAME, String.format("Cannot selectDocument using parameters writable: %s, type: %s, title: %s and extra types: %s. Activity is null.", writable, type, title, extraTypes == null ? null : Arrays.toString(extraTypes))); + resultHandler.errorAsync(result, "INVALID_ACTIVITY", "Activity is null."); + } + } else { + Log.w(LIBRARY_NAME, String.format("Cannot selectDocument using parameters writable: %s, type: %s, title: %s and extra types: %s. Context is null.", writable, type, title, extraTypes == null ? null : Arrays.toString(extraTypes))); + resultHandler.errorAsync(result, "INVALID_CONTEXT", "Context is null."); + } + } + + protected void getSafParameter(@NonNull final String uriString, @NonNull final String openMode, @NonNull final Result result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + Log.i(LIBRARY_NAME, String.format(Locale.getDefault(), "getSafParameter is not supported on API Level %d", Build.VERSION.SDK_INT)); + resultHandler.errorAsync(result, "GET_SAF_PARAMETER_FAILED", String.format(Locale.getDefault(), "getSafParameter is not supported on API Level %d", Build.VERSION.SDK_INT)); + return; + } + + if (context != null) { + final Uri uri = Uri.parse(uriString); + if (uri == null) { + Log.w(LIBRARY_NAME, String.format("Cannot getSafParameter using parameters uriString: %s, openMode: %s. Uri string cannot be parsed.", uriString, openMode)); + resultHandler.errorAsync(result, "GET_SAF_PARAMETER_FAILED", "Uri string cannot be parsed."); + } else { + final String safParameter = FFmpegKitConfig.getSafParameter(context, uri, openMode); + + Log.d(LIBRARY_NAME, String.format("getSafParameter using parameters uriString: %s, openMode: %s completed with saf parameter: %s.", uriString, openMode, safParameter)); + + resultHandler.successAsync(result, safParameter); + } + } else { + Log.w(LIBRARY_NAME, String.format("Cannot getSafParameter using parameters uriString: %s, openMode: %s. Context is null.", uriString, openMode)); + resultHandler.errorAsync(result, "INVALID_CONTEXT", "Context is null."); + } + } + + // FFmpegKit + + protected void cancel(@NonNull final Result result) { + FFmpegKit.cancel(); + resultHandler.successAsync(result, null); + } + + protected void cancelSession(@NonNull final Integer sessionId, @NonNull final Result result) { + FFmpegKit.cancel(sessionId.longValue()); + resultHandler.successAsync(result, null); + } + + protected void getFFmpegSessions(@NonNull final Result result) { + resultHandler.successAsync(result, toSessionMapList(FFmpegKit.listSessions())); + } + + // FFprobeKit + + protected void getFFprobeSessions(@NonNull final Result result) { + resultHandler.successAsync(result, toSessionMapList(FFprobeKit.listFFprobeSessions())); + } + + protected void getMediaInformationSessions(@NonNull final Result result) { + resultHandler.successAsync(result, toSessionMapList(FFprobeKit.listMediaInformationSessions())); + } + + // Packages + + protected void getPackageName(@NonNull final Result result) { + resultHandler.successAsync(result, Packages.getPackageName()); + } + + protected void getExternalLibraries(@NonNull final Result result) { + resultHandler.successAsync(result, Packages.getExternalLibraries()); + } + + protected void enableLogs() { + logsEnabled.compareAndSet(false, true); + } + + protected void disableLogs() { + logsEnabled.compareAndSet(true, false); + } + + protected void enableStatistics() { + statisticsEnabled.compareAndSet(false, true); + } + + protected void disableStatistics() { + statisticsEnabled.compareAndSet(true, false); + } + + protected static int toInt(final Level level) { + return (level == null) ? Level.AV_LOG_TRACE.getValue() : level.getValue(); + } + + protected static Map toMap(final Session session) { + if (session == null) { + return null; + } + + final Map sessionMap = new HashMap<>(); + + sessionMap.put(KEY_SESSION_ID, session.getSessionId()); + sessionMap.put(KEY_SESSION_CREATE_TIME, toLong(session.getCreateTime())); + sessionMap.put(KEY_SESSION_START_TIME, toLong(session.getStartTime())); + sessionMap.put(KEY_SESSION_COMMAND, session.getCommand()); + + if (session.isFFmpeg()) { + sessionMap.put(KEY_SESSION_TYPE, SESSION_TYPE_FFMPEG); + } else if (session.isFFprobe()) { + sessionMap.put(KEY_SESSION_TYPE, SESSION_TYPE_FFPROBE); + } else if (session.isMediaInformation()) { + final MediaInformationSession mediaInformationSession = (MediaInformationSession) session; + final MediaInformation mediaInformation = mediaInformationSession.getMediaInformation(); + if (mediaInformation != null) { + sessionMap.put(KEY_SESSION_MEDIA_INFORMATION, toMap(mediaInformation)); + } + sessionMap.put(KEY_SESSION_TYPE, SESSION_TYPE_MEDIA_INFORMATION); + } + + return sessionMap; + } + + protected static long toLong(final Date date) { + if (date != null) { + return date.getTime(); + } else { + return 0; + } + } + + protected static int toInt(final LogRedirectionStrategy logRedirectionStrategy) { + switch (logRedirectionStrategy) { + case ALWAYS_PRINT_LOGS: + return 0; + case PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED: + return 1; + case PRINT_LOGS_WHEN_GLOBAL_CALLBACK_NOT_DEFINED: + return 2; + case PRINT_LOGS_WHEN_SESSION_CALLBACK_NOT_DEFINED: + return 3; + case NEVER_PRINT_LOGS: + default: + return 4; + } + } + + protected static LogRedirectionStrategy toLogRedirectionStrategy(final int value) { + switch (value) { + case 0: + return LogRedirectionStrategy.ALWAYS_PRINT_LOGS; + case 1: + return LogRedirectionStrategy.PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED; + case 2: + return LogRedirectionStrategy.PRINT_LOGS_WHEN_GLOBAL_CALLBACK_NOT_DEFINED; + case 3: + return LogRedirectionStrategy.PRINT_LOGS_WHEN_SESSION_CALLBACK_NOT_DEFINED; + case 4: + default: + return LogRedirectionStrategy.NEVER_PRINT_LOGS; + } + } + + protected static SessionState toSessionState(final int value) { + switch (value) { + case 0: + return SessionState.CREATED; + case 1: + return SessionState.RUNNING; + case 2: + return SessionState.FAILED; + case 3: + default: + return SessionState.COMPLETED; + } + } + + protected static Map toMap(final com.antonkarpenko.ffmpegkit.Log log) { + final HashMap logMap = new HashMap<>(); + + logMap.put(KEY_LOG_SESSION_ID, log.getSessionId()); + logMap.put(KEY_LOG_LEVEL, toInt(log.getLevel())); + logMap.put(KEY_LOG_MESSAGE, log.getMessage()); + + return logMap; + } + + protected static Map toMap(final Statistics statistics) { + final HashMap statisticsMap = new HashMap<>(); + + if (statistics != null) { + statisticsMap.put(KEY_STATISTICS_SESSION_ID, statistics.getSessionId()); + statisticsMap.put(KEY_STATISTICS_VIDEO_FRAME_NUMBER, statistics.getVideoFrameNumber()); + statisticsMap.put(KEY_STATISTICS_VIDEO_FPS, statistics.getVideoFps()); + statisticsMap.put(KEY_STATISTICS_VIDEO_QUALITY, statistics.getVideoQuality()); + statisticsMap.put(KEY_STATISTICS_SIZE, (statistics.getSize() < Integer.MAX_VALUE) ? (int) statistics.getSize() : (int) (statistics.getSize() % Integer.MAX_VALUE)); + statisticsMap.put(KEY_STATISTICS_TIME, statistics.getTime()); + statisticsMap.put(KEY_STATISTICS_BITRATE, statistics.getBitrate()); + statisticsMap.put(KEY_STATISTICS_SPEED, statistics.getSpeed()); + } + + return statisticsMap; + } + + protected static Map toMap(final MediaInformation mediaInformation) { + if (mediaInformation != null) { + Map map = new HashMap<>(); + + if (mediaInformation.getAllProperties() != null) { + JSONObject allProperties = mediaInformation.getAllProperties(); + if (allProperties != null) { + map = toMap(allProperties); + } + } + + return map; + } else { + return null; + } + } + + protected static Map toMap(final JSONObject jsonObject) { + final HashMap map = new HashMap<>(); + + if (jsonObject != null) { + Iterator keys = jsonObject.keys(); + while (keys.hasNext()) { + String key = keys.next(); + Object value = jsonObject.opt(key); + if (value != null) { + if (value instanceof JSONArray) { + value = toList((JSONArray) value); + } else if (value instanceof JSONObject) { + value = toMap((JSONObject) value); + } + map.put(key, value); + } + } + } + + return map; + } + + protected static List toList(final JSONArray array) { + final List list = new ArrayList<>(); + + for (int i = 0; i < array.length(); i++) { + Object value = array.opt(i); + if (value != null) { + if (value instanceof JSONArray) { + value = toList((JSONArray) value); + } else if (value instanceof JSONObject) { + value = toMap((JSONObject) value); + } + list.add(value); + } + } + + return list; + } + + protected static List> toSessionMapList(final List sessionList) { + final List> list = new ArrayList<>(); + + for (int i = 0; i < sessionList.size(); i++) { + list.add(toMap(sessionList.get(i))); + } + + return list; + } + + protected static List> toLogMapList(final List logList) { + final List> list = new ArrayList<>(); + + for (int i = 0; i < logList.size(); i++) { + list.add(toMap(logList.get(i))); + } + + return list; + } + + protected static List> toStatisticsMapList(final List statisticsList) { + final List> list = new ArrayList<>(); + + for (int i = 0; i < statisticsList.size(); i++) { + list.add(toMap(statisticsList.get(i))); + } + + return list; + } + + protected static boolean isValidPositiveNumber(final Integer value) { + return (value != null) && (value >= 0); + } + + protected void emitLog(final com.antonkarpenko.ffmpegkit.Log log) { + final HashMap logMap = new HashMap<>(); + logMap.put(EVENT_LOG_CALLBACK_EVENT, toMap(log)); + resultHandler.successAsync(eventSink, logMap); + } + + protected void emitStatistics(final Statistics statistics) { + final HashMap statisticsMap = new HashMap<>(); + statisticsMap.put(EVENT_STATISTICS_CALLBACK_EVENT, toMap(statistics)); + resultHandler.successAsync(eventSink, statisticsMap); + } + + protected void emitSession(final Session session) { + final HashMap sessionMap = new HashMap<>(); + sessionMap.put(EVENT_COMPLETE_CALLBACK_EVENT, toMap(session)); + resultHandler.successAsync(eventSink, sessionMap); + } + +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegSessionExecuteTask.java b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegSessionExecuteTask.java new file mode 100644 index 0000000..0442c1a --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFmpegSessionExecuteTask.java @@ -0,0 +1,25 @@ +package com.antonkarpenko.ffmpegkit; + +import androidx.annotation.NonNull; + +import com.antonkarpenko.ffmpegkit.FFmpegKitConfig; + +import io.flutter.plugin.common.MethodChannel; + +public class FFmpegSessionExecuteTask implements Runnable { + private final com.antonkarpenko.ffmpegkit.FFmpegSession ffmpegSession; + private final FFmpegKitFlutterMethodResultHandler resultHandler; + private final MethodChannel.Result result; + + public FFmpegSessionExecuteTask(@NonNull final com.antonkarpenko.ffmpegkit.FFmpegSession ffmpegSession, @NonNull final FFmpegKitFlutterMethodResultHandler resultHandler, @NonNull final MethodChannel.Result result) { + this.ffmpegSession = ffmpegSession; + this.resultHandler = resultHandler; + this.result = result; + } + + @Override + public void run() { + FFmpegKitConfig.ffmpegExecute(ffmpegSession); + resultHandler.successAsync(result, null); + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFprobeSessionExecuteTask.java b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFprobeSessionExecuteTask.java new file mode 100644 index 0000000..c47c84c --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/FFprobeSessionExecuteTask.java @@ -0,0 +1,26 @@ +package com.antonkarpenko.ffmpegkit; + +import androidx.annotation.NonNull; + +import com.antonkarpenko.ffmpegkit.FFmpegKitConfig; +import com.antonkarpenko.ffmpegkit.FFprobeSession; + +import io.flutter.plugin.common.MethodChannel; + +public class FFprobeSessionExecuteTask implements Runnable { + private final FFprobeSession ffprobeSession; + private final FFmpegKitFlutterMethodResultHandler resultHandler; + private final MethodChannel.Result result; + + public FFprobeSessionExecuteTask(@NonNull final FFprobeSession ffprobeSession, @NonNull final FFmpegKitFlutterMethodResultHandler resultHandler, @NonNull final MethodChannel.Result result) { + this.ffprobeSession = ffprobeSession; + this.resultHandler = resultHandler; + this.result = result; + } + + @Override + public void run() { + FFmpegKitConfig.ffprobeExecute(ffprobeSession); + resultHandler.successAsync(result, null); + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/MediaInformationSessionExecuteTask.java b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/MediaInformationSessionExecuteTask.java new file mode 100644 index 0000000..3c3e257 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/MediaInformationSessionExecuteTask.java @@ -0,0 +1,28 @@ +package com.antonkarpenko.ffmpegkit; + +import androidx.annotation.NonNull; + +import com.antonkarpenko.ffmpegkit.FFmpegKitConfig; +import com.antonkarpenko.ffmpegkit.MediaInformationSession; + +import io.flutter.plugin.common.MethodChannel; + +public class MediaInformationSessionExecuteTask implements Runnable { + private final MediaInformationSession mediaInformationSession; + private final int timeout; + private final FFmpegKitFlutterMethodResultHandler resultHandler; + private final MethodChannel.Result result; + + public MediaInformationSessionExecuteTask(@NonNull final MediaInformationSession mediaInformationSession, final int timeout, @NonNull final FFmpegKitFlutterMethodResultHandler resultHandler, final MethodChannel.Result result) { + this.mediaInformationSession = mediaInformationSession; + this.timeout = timeout; + this.resultHandler = resultHandler; + this.result = result; + } + + @Override + public void run() { + FFmpegKitConfig.getMediaInformationExecute(mediaInformationSession, timeout); + resultHandler.successAsync(result, null); + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/WriteToPipeTask.java b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/WriteToPipeTask.java new file mode 100644 index 0000000..d6b03d3 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/src/main/java/com/antonkarpenko/ffmpegkit/WriteToPipeTask.java @@ -0,0 +1,51 @@ +package com.antonkarpenko.ffmpegkit; + +import static com.antonkarpenko.ffmpegkit.FFmpegKitFlutterPlugin.LIBRARY_NAME; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.IOException; + +import io.flutter.plugin.common.MethodChannel; + +public class WriteToPipeTask implements Runnable { + private final String inputPath; + private final String namedPipePath; + private final FFmpegKitFlutterMethodResultHandler resultHandler; + private final MethodChannel.Result result; + + public WriteToPipeTask(@NonNull final String inputPath, @NonNull final String namedPipePath, @NonNull final FFmpegKitFlutterMethodResultHandler resultHandler, @NonNull final MethodChannel.Result result) { + this.inputPath = inputPath; + this.namedPipePath = namedPipePath; + this.resultHandler = resultHandler; + this.result = result; + } + + @Override + public void run() { + final int rc; + + try { + final String asyncCommand = "cat " + inputPath + " > " + namedPipePath; + Log.d(LIBRARY_NAME, String.format("Starting copy %s to pipe %s operation.", inputPath, namedPipePath)); + + final long startTime = System.currentTimeMillis(); + + final Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", asyncCommand}); + rc = process.waitFor(); + + final long endTime = System.currentTimeMillis(); + + Log.d(LIBRARY_NAME, String.format("Copying %s to pipe %s operation completed with rc %d in %d seconds.", inputPath, namedPipePath, rc, (endTime - startTime) / 1000)); + + resultHandler.successAsync(result, rc); + + } catch (final IOException | InterruptedException e) { + Log.e(LIBRARY_NAME, String.format("Copy %s to pipe %s failed with error.", inputPath, namedPipePath), e); + resultHandler.errorAsync(result, "WRITE_TO_PIPE_FAILED", e.getMessage()); + } + } + +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/abstract_session.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/abstract_session.dart new file mode 100644 index 0000000..4cbb521 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/abstract_session.dart @@ -0,0 +1,445 @@ +/* + * Copyright (c) 2019-2022 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart'; +import 'package:flutter/services.dart'; + +import 'ffmpeg_kit_config.dart'; +import 'ffmpeg_session.dart'; +import 'ffprobe_session.dart'; +import 'log.dart'; +import 'log_callback.dart'; +import 'log_redirection_strategy.dart'; +import 'media_information.dart'; +import 'media_information_session.dart'; +import 'return_code.dart'; +import 'session.dart'; +import 'session_state.dart'; +import 'src/ffmpeg_kit_factory.dart'; + +/// Abstract session implementation which includes common features shared by +/// "FFmpeg", "FFprobe" and "MediaInformation" sessions. +abstract class AbstractSession extends Session { + static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance; + + /// Defines how long default "getAll" methods wait, in milliseconds. + static const defaultTimeoutForAsynchronousMessagesInTransmit = 5000; + + /// Session identifier. + int? _sessionId; + + /// Date and time the session was created. + DateTime? _createTime; + + /// Date and time the session was started. + DateTime? _startTime; + + /// Command string. + String? _command; + + /// Command arguments as an array. + List? _argumentsArray; + + /// Session specific log redirection strategy. + LogRedirectionStrategy? _logRedirectionStrategy; + + /// Creates a new FFmpeg session using [argumentsArray] and + /// [logRedirectionStrategy]. + /// + /// Returns FFmpeg session created. + static Future createFFmpegSession(List argumentsArray, + [LogRedirectionStrategy? logRedirectionStrategy = null]) async { + try { + await FFmpegKitConfig.init(); + final Map? nativeSession = + await _platform.abstractSessionCreateFFmpegSession(argumentsArray); + + final session = new FFmpegSession(); + + session._sessionId = nativeSession?["sessionId"]; + session._createTime = + FFmpegKitFactory.validDate(nativeSession?["createTime"]); + session._startTime = + FFmpegKitFactory.validDate(nativeSession?["startTime"]); + session._command = nativeSession?["command"]; + session._argumentsArray = argumentsArray; + session._logRedirectionStrategy = + logRedirectionStrategy ?? FFmpegKitConfig.getLogRedirectionStrategy(); + + FFmpegKitFactory.setLogRedirectionStrategy( + session._sessionId, logRedirectionStrategy); + + return session; + } on PlatformException catch (e, stack) { + print("Plugin createFFmpegSession error: ${e.message}"); + return Future.error("createFFmpegSession failed.", stack); + } + } + + /// Creates a new FFmpeg session from [sessionMap], which includes session + /// fields as map keys. + /// + /// Returns FFmpeg session created. + static FFmpegSession createFFmpegSessionFromMap( + Map sessionMap) { + final session = new FFmpegSession(); + + session._sessionId = sessionMap["sessionId"]; + session._createTime = FFmpegKitFactory.validDate(sessionMap["createTime"]); + session._startTime = FFmpegKitFactory.validDate(sessionMap["startTime"]); + session._command = sessionMap["command"]; + session._argumentsArray = + FFmpegKitConfig.parseArguments(sessionMap["command"]); + session._logRedirectionStrategy = + FFmpegKitFactory.getLogRedirectionStrategy(session._sessionId); + + return session; + } + + /// Creates a new FFprobe session using [argumentsArray] and + /// [logRedirectionStrategy]. + /// + /// Returns FFprobe session created. + static Future createFFprobeSession( + List argumentsArray, + [LogRedirectionStrategy? logRedirectionStrategy = null]) async { + try { + await FFmpegKitConfig.init(); + final Map? nativeSession = + await _platform.abstractSessionCreateFFprobeSession(argumentsArray); + + final session = new FFprobeSession(); + + session._sessionId = nativeSession?["sessionId"]; + session._createTime = + FFmpegKitFactory.validDate(nativeSession?["createTime"]); + session._startTime = + FFmpegKitFactory.validDate(nativeSession?["startTime"]); + session._command = nativeSession?["command"]; + session._argumentsArray = argumentsArray; + session._logRedirectionStrategy = + logRedirectionStrategy ?? FFmpegKitConfig.getLogRedirectionStrategy(); + + FFmpegKitFactory.setLogRedirectionStrategy( + session._sessionId, logRedirectionStrategy); + + return session; + } on PlatformException catch (e, stack) { + print("Plugin createFFprobeSession error: ${e.message}"); + return Future.error("createFFprobeSession failed.", stack); + } + } + + /// Creates a new FFprobe session from [sessionMap], which includes session + /// fields as map keys. + /// + /// Returns FFprobe session created. + static FFprobeSession createFFprobeSessionFromMap( + Map sessionMap) { + final session = new FFprobeSession(); + + session._sessionId = sessionMap["sessionId"]; + session._createTime = FFmpegKitFactory.validDate(sessionMap["createTime"]); + session._startTime = FFmpegKitFactory.validDate(sessionMap["startTime"]); + session._command = sessionMap["command"]; + session._argumentsArray = + FFmpegKitConfig.parseArguments(sessionMap["command"]); + session._logRedirectionStrategy = + FFmpegKitFactory.getLogRedirectionStrategy(session._sessionId); + + return session; + } + + /// Creates a new MediaInformation session using [argumentsArray]. + /// + /// Returns MediaInformation session created. + static Future createMediaInformationSession( + List argumentsArray) async { + try { + await FFmpegKitConfig.init(); + final Map? nativeSession = await _platform + .abstractSessionCreateMediaInformationSession(argumentsArray); + final session = new MediaInformationSession(); + + session._sessionId = nativeSession?["sessionId"]; + session._createTime = + FFmpegKitFactory.validDate(nativeSession?["createTime"]); + session._startTime = + FFmpegKitFactory.validDate(nativeSession?["startTime"]); + session._command = nativeSession?["command"]; + session._argumentsArray = argumentsArray; + session._logRedirectionStrategy = LogRedirectionStrategy.neverPrintLogs; + + FFmpegKitFactory.setLogRedirectionStrategy( + session._sessionId, LogRedirectionStrategy.neverPrintLogs); + + return session; + } on PlatformException catch (e, stack) { + print("Plugin createMediaInformationSession error: ${e.message}"); + return Future.error("createMediaInformationSession failed.", stack); + } + } + + /// Creates a new MediaInformation session from [sessionMap], which includes + /// session fields as map keys. + /// + /// Returns MediaInformation session created. + static MediaInformationSession createMediaInformationSessionFromMap( + Map sessionMap) { + final session = new MediaInformationSession(); + + session._sessionId = sessionMap["sessionId"]; + session._createTime = FFmpegKitFactory.validDate(sessionMap["createTime"]); + session._startTime = FFmpegKitFactory.validDate(sessionMap["startTime"]); + session._command = sessionMap["command"]; + session._argumentsArray = + FFmpegKitConfig.parseArguments(sessionMap["command"]); + session._logRedirectionStrategy = LogRedirectionStrategy.neverPrintLogs; + + if (sessionMap.containsKey("mediaInformation")) { + session.setMediaInformation( + new MediaInformation(sessionMap["mediaInformation"])); + } + + return session; + } + + /// Returns the session specific log callback. + LogCallback? getLogCallback() => + FFmpegKitFactory.getLogCallback(this.getSessionId()); + + /// Returns the session identifier. + int? getSessionId() => this._sessionId; + + /// Returns session create time. + DateTime? getCreateTime() => this._createTime; + + /// Returns session start time. + DateTime? getStartTime() => this._startTime; + + /// Returns session end time. + Future getEndTime() async { + try { + return _platform + .abstractSessionGetEndTime(this.getSessionId()) + .then(FFmpegKitFactory.validDate); + } on PlatformException catch (e, stack) { + print("Plugin getEndTime error: ${e.message}"); + return Future.error("getEndTime failed.", stack); + } + } + + /// Returns time taken to execute this session in milliseconds or zero (0) + /// if the session is not over yet. + Future getDuration() async { + try { + return _platform + .abstractSessionGetDuration(this.getSessionId()) + .then((duration) => duration ?? 0); + } on PlatformException catch (e, stack) { + print("Plugin getDuration error: ${e.message}"); + return Future.error("getDuration failed.", stack); + } + } + + /// Returns command arguments as an array. + List? getArguments() => this._argumentsArray; + + /// Returns command arguments as a concatenated string. + String? getCommand() => this._command; + + /// Returns all log entries generated for this session. If there are + /// asynchronous logs that are not delivered yet, this method waits for + /// them until [waitTimeout]. + Future> getAllLogs([int? waitTimeout = null]) async { + try { + return _platform + .abstractSessionGetAllLogs(this.getSessionId(), waitTimeout) + .then((allLogs) { + if (allLogs == null) { + return List.empty(); + } else { + return allLogs + .map((dynamic logObject) => + FFmpegKitFactory.mapToLog(logObject as Map)) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getAllLogs error: ${e.message}"); + return Future.error("getAllLogs failed.", stack); + } + } + + /// Returns all log entries delivered for this session. Note that if there + /// are asynchronous logs that are not delivered yet, this method + /// will not wait for them and will return immediately. + Future> getLogs() async { + try { + return _platform + .abstractSessionGetLogs(this.getSessionId()) + .then((allLogs) { + if (allLogs == null) { + return List.empty(); + } else { + return allLogs + .map((dynamic logObject) => + FFmpegKitFactory.mapToLog(logObject as Map)) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getLogs error: ${e.message}"); + return Future.error("getLogs failed.", stack); + } + } + + /// Returns all log entries generated for this session as a concatenated + /// string. If there are asynchronous logs that are not delivered yet, + /// this method waits for them until [waitTimeout]. + Future getAllLogsAsString([int? waitTimeout = null]) async { + try { + return _platform.abstractSessionGetAllLogsAsString( + this.getSessionId(), waitTimeout); + } on PlatformException catch (e, stack) { + print("Plugin getAllLogsAsString error: ${e.message}"); + return Future.error("getAllLogsAsString failed.", stack); + } + } + + /// Returns all log entries delivered for this session as a concatenated + /// string. Note that if there are asynchronous logs that are not + /// delivered yet, this method will not wait for them and will return + /// immediately. + Future getLogsAsString() async { + final StringBuffer concatenatedString = new StringBuffer(); + + void concatLog(Log log) => concatenatedString.write(log.getMessage()); + + final List logs = await this.getLogs(); + + logs.forEach(concatLog); + + return concatenatedString.toString(); + } + + /// Returns the log output generated while running the session. + Future getOutput() async => this.getAllLogsAsString(); + + /// Returns the state of the session. + Future getState() async { + try { + return _platform + .abstractSessionGetState(this.getSessionId()) + .then((state) { + switch (state) { + case 0: + return SessionState.created; + case 1: + return SessionState.running; + case 2: + return SessionState.failed; + case 3: + default: + return SessionState.completed; + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getState error: ${e.message}"); + return Future.error("getState failed.", stack); + } + } + + /// Returns the return code for this session. Note that return code is only + /// set for sessions that end with COMPLETED state. If a session is not + /// started, still running or failed then this method returns null. + Future getReturnCode() async { + try { + return _platform + .abstractSessionGetReturnCode(this.getSessionId()) + .then((returnCode) { + if (returnCode == null) { + return null; + } else { + return new ReturnCode(returnCode); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getReturnCode error: ${e.message}"); + return Future.error("getReturnCode failed.", stack); + } + } + + /// Returns the stack trace of the exception received while executing this + /// session. + /// + /// The stack trace is only set for sessions that end with FAILED state. For + /// sessions that has COMPLETED state this method returns null. + Future getFailStackTrace() async { + try { + return _platform.abstractSessionGetFailStackTrace(this.getSessionId()); + } on PlatformException catch (e, stack) { + print("Plugin getFailStackTrace error: ${e.message}"); + return Future.error("getFailStackTrace failed.", stack); + } + } + + /// Returns session specific log redirection strategy. + LogRedirectionStrategy? getLogRedirectionStrategy() => + this._logRedirectionStrategy; + + /// Returns whether there are still asynchronous messages being transmitted + /// for this session or not. + Future thereAreAsynchronousMessagesInTransmit() async { + try { + return _platform.abstractSessionThereAreAsynchronousMessagesInTransmit( + this.getSessionId()); + } on PlatformException catch (e, stack) { + print( + "Plugin thereAreAsynchronousMessagesInTransmit error: ${e.message}"); + return Future.error( + "thereAreAsynchronousMessagesInTransmit failed.", stack); + } + } + + /// Returns whether it is an "FFmpeg" session or not. + bool isFFmpeg() => false; + + /// Returns whether it is an "FFprobe" session or not. + bool isFFprobe() => false; + + /// Returns whether it is an "MediaInformation" session or not. + bool isMediaInformation() => false; + + /// Cancels running the session. + Future cancel() async { + try { + final int? sessionId = getSessionId(); + await FFmpegKitConfig.init(); + if (sessionId == null) { + return _platform.ffmpegKitCancel(); + } else { + return _platform.ffmpegKitCancelSession(sessionId); + } + } on PlatformException catch (e, stack) { + print("Plugin cancel error: ${e.message}"); + return Future.error("cancel failed.", stack); + } + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/arch_detect.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/arch_detect.dart new file mode 100644 index 0000000..3553ea1 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/arch_detect.dart @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart'; +import 'package:flutter/services.dart'; + +import 'ffmpeg_kit_config.dart'; + +/// Detects the running architecture. +class ArchDetect { + static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance; + + /// Returns architecture name loaded. + static Future getArch() async { + try { + await FFmpegKitConfig.init(); + return _platform.archDetectGetArch(); + } on PlatformException catch (e, stack) { + print("Plugin getArch error: ${e.message}"); + return Future.error("getArch failed.", stack); + } + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/chapter.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/chapter.dart new file mode 100644 index 0000000..cdf6b87 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/chapter.dart @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2021-2022 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +/// Chapter class. +class Chapter { + static const keyId = "id"; + static const keyTimeBase = "time_base"; + static const keyStart = "start"; + static const keyStartTime = "start_time"; + static const keyEnd = "end"; + static const keyEndTime = "end_time"; + static const keyTags = "tags"; + + Map? _allProperties; + + /// Creates a new [Chapter] instance + Chapter(this._allProperties); + + /// Returns id. + int? getId() => this.getNumberProperty(Chapter.keyId)?.toInt(); + + /// Returns time base. + String? getTimeBase() => this.getStringProperty(Chapter.keyTimeBase); + + /// Returns start. + int? getStart() => this.getNumberProperty(Chapter.keyStart)?.toInt(); + + /// Returns start time. + String? getStartTime() => this.getStringProperty(Chapter.keyStartTime); + + /// Returns end. + int? getEnd() => this.getNumberProperty(Chapter.keyEnd)?.toInt(); + + /// Returns end time. + String? getEndTime() => this.getStringProperty(Chapter.keyEndTime); + + /// Returns all tags. + Map? getTags() => this.getProperty(Chapter.keyTags); + + /// Returns the chapter property associated with the key. + String? getStringProperty(String key) => this._allProperties?[key]; + + /// Returns the chapter property associated with the key. + num? getNumberProperty(String key) => this._allProperties?[key]; + + /// Returns the chapter property associated with the key. + dynamic getProperty(String key) => this._allProperties?[key]; + + /// Returns all properties found. + Map? getAllProperties() => this._allProperties; +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/extensions.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/extensions.dart new file mode 100644 index 0000000..d06ccff --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/extensions.dart @@ -0,0 +1,26 @@ +import 'ffmpeg_session.dart'; +import 'return_code.dart'; + +extension XFFmpegSession on Future { + Future thenReturnResultOrLogs( + String Function(FFmpegSession) onSuccess, + ) => + then( + (session) => session.getReturnCode().then( + (returnCode) { + if (ReturnCode.isSuccess(returnCode)) { + return onSuccess(session); + } + return session.getAllLogs().then( + (logs) { + final String logOutput = + logs.map((log) => log.getMessage()).join('\n'); + throw Exception( + 'FFmpeg command failed with return code: $returnCode. Logs: $logOutput', + ); + }, + ); + }, + ), + ); +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_kit.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_kit.dart new file mode 100644 index 0000000..2126ee0 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_kit.dart @@ -0,0 +1,105 @@ +import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart'; +import 'package:flutter/services.dart'; + +import 'ffmpeg_kit_config.dart'; +import 'ffmpeg_session.dart'; +import 'ffmpeg_session_complete_callback.dart'; +import 'log_callback.dart'; +import 'src/ffmpeg_kit_factory.dart'; +import 'statistics_callback.dart'; + +/// Main class to run "FFmpeg" commands. +class FFmpegKit { + static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance; + + /// Synchronously executes FFmpeg command provided. Space character is used + /// to split command into arguments. You can use single or double quote + /// characters to specify arguments inside your command. + static Future execute(String command) async => + FFmpegKit.executeWithArguments(FFmpegKitConfig.parseArguments(command)); + + /// Synchronously executes FFmpeg with arguments provided. + static Future executeWithArguments( + List commandArguments) async { + final session = + await FFmpegSession.create(commandArguments, null, null, null, null); + + await FFmpegKitConfig.ffmpegExecute(session); + + return session; + } + + /// Starts an asynchronous FFmpeg execution for the given command. + /// Space character is used to split the command + /// into arguments. You can use single or double quote characters + /// to specify arguments inside your command. + /// + /// Note that this method returns immediately and does not wait + /// the execution to complete. You must use an + /// [FFmpegSessionCompleteCallback] if you want to be notified + /// about the result. + static Future executeAsync(String command, + [FFmpegSessionCompleteCallback? completeCallback = null, + LogCallback? logCallback = null, + StatisticsCallback? statisticsCallback = null]) async => + FFmpegKit.executeWithArgumentsAsync( + FFmpegKitConfig.parseArguments(command), + completeCallback, + logCallback, + statisticsCallback); + + /// Starts an asynchronous FFmpeg execution with arguments provided. + /// + /// Note that this method returns immediately and does not + /// wait the execution to complete. You must use an + /// [FFmpegSessionCompleteCallback] if you want to be notified + /// about the result. + static Future executeWithArgumentsAsync( + List commandArguments, + [FFmpegSessionCompleteCallback? completeCallback = null, + LogCallback? logCallback = null, + StatisticsCallback? statisticsCallback = null]) async { + final session = await FFmpegSession.create(commandArguments, + completeCallback, logCallback, statisticsCallback, null); + + await FFmpegKitConfig.asyncFFmpegExecute(session); + + return session; + } + + /// Cancels the session specified with [sessionId]. + static Future cancel([int? sessionId = null]) async { + try { + await FFmpegKitConfig.init(); + if (sessionId == null) { + return _platform.ffmpegKitCancel(); + } else { + return _platform.ffmpegKitCancelSession(sessionId); + } + } on PlatformException catch (e, stack) { + print("Plugin cancel error: ${e.message}"); + return Future.error("cancel failed.", stack); + } + } + + /// Lists all FFmpeg sessions in the session history. + static Future> listSessions() async { + try { + await FFmpegKitConfig.init(); + return _platform.ffmpegKitListSessions().then((sessions) { + if (sessions == null) { + return List.empty(); + } else { + return sessions + .map((dynamic sessionObject) => FFmpegKitFactory.mapToSession( + sessionObject as Map)) + .map((session) => session as FFmpegSession) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin listSessions error: ${e.message}"); + return Future.error("listSessions failed.", stack); + } + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_kit_config.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_kit_config.dart new file mode 100644 index 0000000..2b2a1fb --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_kit_config.dart @@ -0,0 +1,820 @@ +/* + * Copyright (c) 2019-2022 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart'; +import 'package:flutter/services.dart'; + +import 'ffmpeg_session.dart'; +import 'ffmpeg_session_complete_callback.dart'; +import 'ffprobe_session.dart'; +import 'ffprobe_session_complete_callback.dart'; +import 'level.dart'; +import 'log_callback.dart'; +import 'log_redirection_strategy.dart'; +import 'media_information_session.dart'; +import 'media_information_session_complete_callback.dart'; +import 'session.dart'; +import 'session_state.dart'; +import 'signal.dart'; +import 'src/ffmpeg_kit_factory.dart'; +import 'src/ffmpeg_kit_flutter_initializer.dart'; +import 'statistics_callback.dart'; + +/// Configuration class of "FFmpegKit" library. +class FFmpegKitConfig { + static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance; + + static LogRedirectionStrategy _globalLogRedirectionStrategy = + LogRedirectionStrategy.printLogsWhenNoCallbacksDefined; + + static int _activeLogLevel = Level.avLogTrace; + + /// Initializes the library asynchronously. + static Future init() async { + await FFmpegKitInitializer.initialize(); + } + + /// Enables log and statistics redirection. + /// + /// When redirection is enabled FFmpeg/FFprobe logs are redirected to console + /// and sessions collect log and statistics entries for the executions. It + /// is possible to define global or session specific log/statistics callbacks + /// as well. + /// + /// Note that redirection is enabled by default. If you do not want to use + /// its functionality please use [disableRedirection] to disable it. + static Future enableRedirection() async { + try { + await init(); + return _platform.ffmpegKitConfigEnableRedirection(); + } on PlatformException catch (e, stack) { + print("Plugin enableRedirection error: ${e.message}"); + return Future.error("enableRedirection failed.", stack); + } + } + + /// Disables log and statistics redirection. + /// + /// When redirection is disabled logs are printed to stderr, all logs and + /// statistics callbacks are disabled and "FFprobeKit.getMediaInformation" + /// methods do not work. + static Future disableRedirection() async { + try { + await init(); + return _platform.ffmpegKitConfigDisableRedirection(); + } on PlatformException catch (e, stack) { + print("Plugin disableRedirection error: ${e.message}"); + return Future.error("disableRedirection failed.", stack); + } + } + + /// Sets and overrides "fontconfig" configuration directory. + static Future setFontconfigConfigurationPath(String path) async { + try { + await init(); + return _platform.ffmpegKitConfigSetFontconfigConfigurationPath(path); + } on PlatformException catch (e, stack) { + print("Plugin setFontconfigConfigurationPath error: ${e.message}"); + return Future.error("setFontconfigConfigurationPath failed.", stack); + } + } + + /// Registers the fonts inside the given path, so they become available to + /// use in FFmpeg filters. + /// + /// Note that you need to use a package with "fontconfig" inside to be able + /// to use fonts in "FFmpeg". + static Future setFontDirectory(String path, + [Map? mapping = null]) async { + try { + await init(); + return _platform.ffmpegKitConfigSetFontDirectory(path, mapping); + } on PlatformException catch (e, stack) { + print("Plugin setFontDirectory error: ${e.message}"); + return Future.error("setFontDirectory failed.", stack); + } + } + + /// Registers the fonts inside the given list of font directories, so they + /// become available to use in FFmpeg filters. + /// + /// Note that you need to use a package with "fontconfig" inside to be able + /// to use fonts in "FFmpeg". + static Future setFontDirectoryList(List fontDirectoryList, + [Map? mapping = null]) async { + try { + await init(); + return _platform.ffmpegKitConfigSetFontDirectoryList( + fontDirectoryList, mapping); + } on PlatformException catch (e, stack) { + print("Plugin setFontDirectoryList error: ${e.message}"); + return Future.error("setFontDirectoryList failed.", stack); + } + } + + /// Creates a new named pipe to use in "FFmpeg" operations. + /// + /// Please note that creator is responsible of closing created pipes. + static Future registerNewFFmpegPipe() async { + try { + await init(); + return _platform.ffmpegKitConfigRegisterNewFFmpegPipe(); + } on PlatformException catch (e, stack) { + print("Plugin registerNewFFmpegPipe error: ${e.message}"); + return Future.error("registerNewFFmpegPipe failed.", stack); + } + } + + /// Closes a previously created "FFmpeg" pipe. + static Future closeFFmpegPipe(String ffmpegPipePath) async { + try { + await init(); + return _platform.ffmpegKitConfigCloseFFmpegPipe(ffmpegPipePath); + } on PlatformException catch (e, stack) { + print("Plugin closeFFmpegPipe error: ${e.message}"); + return Future.error("closeFFmpegPipe failed.", stack); + } + } + + /// Returns the version of FFmpeg bundled within "FFmpegKit" library. + static Future getFFmpegVersion() async { + try { + await init(); + return _platform.ffmpegKitConfigGetFFmpegVersion(); + } on PlatformException catch (e, stack) { + print("Plugin getFFmpegVersion error: ${e.message}"); + return Future.error("getFFmpegVersion failed.", stack); + } + } + + /// Returns FFmpegKit Flutter library version. + static Future getVersion() async => FFmpegKitFactory.getVersion(); + + /// Returns whether FFmpegKit release is a Long Term Release or not. + static Future isLTSBuild() async { + try { + await init(); + return _platform.ffmpegKitConfigIsLTSBuild().then((value) { + if (value == null) { + return false; + } else { + return value; + } + }); + } on PlatformException catch (e, stack) { + print("Plugin isLTSBuild error: ${e.message}"); + return Future.error("isLTSBuild failed.", stack); + } + } + + /// Returns FFmpegKit native library build date. + static Future getBuildDate() async { + try { + await init(); + return _platform.ffmpegKitConfigGetBuildDate(); + } on PlatformException catch (e, stack) { + print("Plugin getBuildDate error: ${e.message}"); + return Future.error("getBuildDate failed.", stack); + } + } + + /// Sets an environment variable. + static Future setEnvironmentVariable(String name, String value) async { + try { + await init(); + return _platform.ffmpegKitConfigSetEnvironmentVariable(name, value); + } on PlatformException catch (e, stack) { + print("Plugin setEnvironmentVariable error: ${e.message}"); + return Future.error("setEnvironmentVariable failed.", stack); + } + } + + /// Registers a new ignored signal. Ignored signals are not handled by + /// "FFmpegKit" library. + static Future ignoreSignal(Signal signal) async { + try { + await init(); + return _platform.ffmpegKitConfigIgnoreSignal(signal.index); + } on PlatformException catch (e, stack) { + print("Plugin ignoreSignal error: ${e.message}"); + return Future.error("ignoreSignal failed.", stack); + } + } + + /// Synchronously executes the FFmpeg session provided. + static Future ffmpegExecute(FFmpegSession ffmpegSession) async { + try { + await init(); + return _platform + .ffmpegKitConfigFFmpegExecute(ffmpegSession.getSessionId()); + } on PlatformException catch (e, stack) { + print("Plugin ffmpegExecute error: ${e.message}"); + return Future.error("ffmpegExecute failed.", stack); + } + } + + /// Synchronously executes the FFprobe session provided. + static Future ffprobeExecute(FFprobeSession ffprobeSession) async { + try { + await init(); + return _platform + .ffmpegKitConfigFFprobeExecute(ffprobeSession.getSessionId()); + } on PlatformException catch (e, stack) { + print("Plugin ffprobeExecute error: ${e.message}"); + return Future.error("ffprobeExecute failed.", stack); + } + } + + /// Synchronously executes the media information session provided. + static Future getMediaInformationExecute( + MediaInformationSession mediaInformationSession, + [int? waitTimeout = null]) async { + try { + await init(); + return _platform.ffmpegKitConfigGetMediaInformationExecute( + mediaInformationSession.getSessionId(), waitTimeout); + } on PlatformException catch (e, stack) { + print("Plugin getMediaInformationExecute error: ${e.message}"); + return Future.error("getMediaInformationExecute failed.", stack); + } + } + + /// Starts an asynchronous FFmpeg execution for the given session. + /// + /// Note that this method returns immediately and does not wait the execution + /// to complete. You must use an + /// [FFmpegSessionCompleteCallback] if you want to be notified about + /// the result. + static Future asyncFFmpegExecute(FFmpegSession ffmpegSession) async { + try { + await init(); + return _platform + .ffmpegKitConfigAsyncFFmpegExecute(ffmpegSession.getSessionId()); + } on PlatformException catch (e, stack) { + print("Plugin asyncFFmpegExecute error: ${e.message}"); + return Future.error("asyncFFmpegExecute failed.", stack); + } + } + + /// Starts an asynchronous FFprobe execution for the given session. + /// + /// Note that this method returns immediately and does not wait the execution + /// to complete. You must use an + /// [FFprobeSessionCompleteCallback] if you want to be notified + /// about the result. + static Future asyncFFprobeExecute(FFprobeSession ffprobeSession) async { + try { + await init(); + return _platform + .ffmpegKitConfigAsyncFFprobeExecute(ffprobeSession.getSessionId()); + } on PlatformException catch (e, stack) { + print("Plugin asyncFFprobeExecute error: ${e.message}"); + return Future.error("asyncFFprobeExecute failed.", stack); + } + } + + /// Starts an asynchronous FFprobe execution for the given media information + /// session. + /// + /// Note that this method returns immediately and does not wait the execution + /// to complete. You must use an + /// [MediaInformationSessionCompleteCallback] if you want to be notified about + /// the result. + static Future asyncGetMediaInformationExecute( + MediaInformationSession mediaInformationSession, + [int? waitTimeout = null]) async { + try { + await init(); + return _platform.ffmpegKitConfigAsyncGetMediaInformationExecute( + mediaInformationSession.getSessionId(), waitTimeout); + } on PlatformException catch (e, stack) { + print("Plugin asyncGetMediaInformationExecute error: ${e.message}"); + return Future.error("asyncGetMediaInformationExecute failed.", stack); + } + } + + /// Sets a global callback to redirect FFmpeg/FFprobe logs. + static void enableLogCallback([LogCallback? logCallback = null]) { + FFmpegKitFactory.setGlobalLogCallback(logCallback); + } + + /// Sets a global callback to redirect FFmpeg statistics. + static void enableStatisticsCallback( + [StatisticsCallback? statisticsCallback = null]) { + FFmpegKitFactory.setGlobalStatisticsCallback(statisticsCallback); + } + + /// Sets a global FFmpegSessionCompleteCallback to receive execution results + /// for FFmpeg sessions. + static void enableFFmpegSessionCompleteCallback( + [FFmpegSessionCompleteCallback? ffmpegSessionCompleteCallback = null]) { + FFmpegKitFactory.setGlobalFFmpegSessionCompleteCallback( + ffmpegSessionCompleteCallback); + } + + /// Returns the global FFmpegSessionCompleteCallback set. + static FFmpegSessionCompleteCallback? getFFmpegSessionCompleteCallback() => + FFmpegKitFactory.getGlobalFFmpegSessionCompleteCallback(); + + /// Sets a global FFprobeSessionCompleteCallback to receive execution results + /// for FFprobe sessions. + static void enableFFprobeSessionCompleteCallback( + [FFprobeSessionCompleteCallback? ffprobeSessionCompleteCallback = null]) { + FFmpegKitFactory.setGlobalFFprobeSessionCompleteCallback( + ffprobeSessionCompleteCallback); + } + + /// Returns the global FFprobeSessionCompleteCallback set. + static FFprobeSessionCompleteCallback? getFFprobeSessionCompleteCallback() => + FFmpegKitFactory.getGlobalFFprobeSessionCompleteCallback(); + + /// Sets a global MediaInformationSessionCompleteCallback to receive + /// execution results for MediaInformation sessions. + static void enableMediaInformationSessionCompleteCallback( + [MediaInformationSessionCompleteCallback? + mediaInformationSessionCompleteCallback = null]) { + FFmpegKitFactory.setGlobalMediaInformationSessionCompleteCallback( + mediaInformationSessionCompleteCallback); + } + + /// Returns the global MediaInformationSessionCompleteCallback set. + static MediaInformationSessionCompleteCallback? + getMediaInformationSessionCompleteCallback() => + FFmpegKitFactory.getGlobalMediaInformationSessionCompleteCallback(); + + /// Returns the current log level. + static int getLogLevel() => _activeLogLevel; + + /// Sets the log level. + static Future setLogLevel(int logLevel) async { + _activeLogLevel = logLevel; + try { + await init(); + return _platform.ffmpegKitConfigSetLogLevel(logLevel); + } on PlatformException catch (e, stack) { + print("Plugin setLogLevel error: ${e.message}"); + return Future.error("setLogLevel failed.", stack); + } + } + + /// Converts the given Structured Access Framework Uri ("content:…") into + /// an input url that can be used in FFmpeg and FFprobe commands. + /// + /// Note that this method is Android only. It will fail if called on other + /// platforms. It also requires API Level ≥ 19. On older API levels it + /// returns an empty url. + static Future getSafParameterForRead(String uriString) async { + try { + await init(); + return _platform.ffmpegKitConfigGetSafParameter(uriString, "r"); + } on PlatformException catch (e, stack) { + print("Plugin getSafParameterForRead error: ${e.message}"); + return Future.error("getSafParameterForRead failed.", stack); + } + } + + /// Converts the given Structured Access Framework Uri ("content:…") into + /// an output url that can be used in FFmpeg and FFprobe commands. + /// + /// Note that this method is Android only. It will fail if called on other + /// platforms. It also requires API Level ≥ 19. On older API levels it + /// returns an empty url. + static Future getSafParameterForWrite(String uriString) async { + try { + await init(); + return _platform.ffmpegKitConfigGetSafParameter(uriString, "w"); + } on PlatformException catch (e, stack) { + print("Plugin getSafParameterForWrite error: ${e.message}"); + return Future.error("getSafParameterForWrite failed.", stack); + } + } + + /// Converts the given Structured Access Framework Uri into an saf protocol + /// url opened with the given open mode. + /// + /// Note that this method is Android only. It will fail if called on other + /// platforms. It also requires API Level ≥ 19. On older API levels it + /// returns an empty url. + static Future getSafParameter( + String uriString, String openMode) async { + try { + await init(); + return _platform.ffmpegKitConfigGetSafParameter(uriString, openMode); + } on PlatformException catch (e, stack) { + print("Plugin getSafParameter error: ${e.message}"); + return Future.error("getSafParameter failed.", stack); + } + } + + /// Returns the session history size. + static Future getSessionHistorySize() async { + try { + await init(); + return _platform.ffmpegKitConfigGetSessionHistorySize(); + } on PlatformException catch (e, stack) { + print("Plugin getSessionHistorySize error: ${e.message}"); + return Future.error("getSessionHistorySize failed.", stack); + } + } + + /// Sets the session history size. + static Future setSessionHistorySize(int sessionHistorySize) async { + try { + await init(); + return _platform.ffmpegKitConfigSetSessionHistorySize(sessionHistorySize); + } on PlatformException catch (e, stack) { + print("Plugin setSessionHistorySize error: ${e.message}"); + return Future.error("setSessionHistorySize failed.", stack); + } + } + + /// Returns the session specified with "sessionId" from the session history. + static Future getSession(int sessionId) async { + try { + await init(); + return _platform + .ffmpegKitConfigGetSession(sessionId) + .then(FFmpegKitFactory.mapToNullableSession); + } on PlatformException catch (e, stack) { + print("Plugin getSession error: ${e.message}"); + return Future.error("getSession failed.", stack); + } + } + + /// Returns the last session created from the session history. + static Future getLastSession() async { + try { + await init(); + return _platform + .ffmpegKitConfigGetLastSession() + .then(FFmpegKitFactory.mapToNullableSession); + } on PlatformException catch (e, stack) { + print("Plugin getLastSession error: ${e.message}"); + return Future.error("getLastSession failed.", stack); + } + } + + /// Returns the last session completed from the session history. + static Future getLastCompletedSession() async { + try { + await init(); + return _platform + .ffmpegKitConfigGetLastCompletedSession() + .then(FFmpegKitFactory.mapToNullableSession); + } on PlatformException catch (e, stack) { + print("Plugin getLastCompletedSession error: ${e.message}"); + return Future.error("getLastCompletedSession failed.", stack); + } + } + + /// Returns all sessions in the session history. + static Future> getSessions() async { + try { + await init(); + return _platform.ffmpegKitConfigGetSessions().then((sessions) { + if (sessions == null) { + return List.empty(); + } else { + return sessions + .map((dynamic sessionObject) => FFmpegKitFactory.mapToSession( + sessionObject as Map)) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getSessions error: ${e.message}"); + return Future.error("getSessions failed.", stack); + } + } + + /// Clears all, including ongoing, sessions in the session history. + /// Note that callbacks cannot be triggered for deleted sessions. + static Future clearSessions() async { + try { + await init(); + return _platform.clearSessions(); + } on PlatformException catch (e, stack) { + print("Plugin clearSessions error: ${e.message}"); + return Future.error("clearSessions failed.", stack); + } + } + + /// Returns all FFmpeg sessions in the session history. + static Future> getFFmpegSessions() async { + try { + await FFmpegKitConfig.init(); + return _platform.ffmpegKitListSessions().then((sessions) { + if (sessions == null) { + return List.empty(); + } else { + return sessions + .map((dynamic sessionObject) => FFmpegKitFactory.mapToSession( + sessionObject as Map)) + .map((session) => session as FFmpegSession) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getFFmpegSessions error: ${e.message}"); + return Future.error("getFFmpegSessions failed.", stack); + } + } + + /// Returns all FFprobe sessions in the session history. + static Future> getFFprobeSessions() async { + try { + await FFmpegKitConfig.init(); + return _platform.ffprobeKitListFFprobeSessions().then((sessions) { + if (sessions == null) { + return List.empty(); + } else { + return sessions + .map((dynamic sessionObject) => FFmpegKitFactory.mapToSession( + sessionObject as Map)) + .map((session) => session as FFprobeSession) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getFFprobeSessions error: ${e.message}"); + return Future.error("getFFprobeSessions failed.", stack); + } + } + + /// Returns all MediaInformation sessions in the session history. + static Future> + getMediaInformationSessions() async { + try { + await FFmpegKitConfig.init(); + return _platform + .ffprobeKitListMediaInformationSessions() + .then((sessions) { + if (sessions == null) { + return List.empty(); + } else { + return sessions + .map((dynamic sessionObject) => FFmpegKitFactory.mapToSession( + sessionObject as Map)) + .map((session) => session as MediaInformationSession) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getMediaInformationSessions error: ${e.message}"); + return Future.error("getMediaInformationSessions failed.", stack); + } + } + + /// Returns sessions that have [sessionState]. + static Future> getSessionsByState( + SessionState sessionState) async { + try { + await init(); + return _platform + .ffmpegKitConfigGetSessionsByState(sessionState.index) + .then((sessions) { + if (sessions == null) { + return List.empty(); + } else { + return sessions + .map((dynamic sessionObject) => FFmpegKitFactory.mapToSession( + sessionObject as Map)) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getSessionsByState error: ${e.message}"); + return Future.error("getSessionsByState failed.", stack); + } + } + + /// Returns the active log redirection strategy. + static LogRedirectionStrategy getLogRedirectionStrategy() => + _globalLogRedirectionStrategy; + + /// Sets the log redirection strategy. + static void setLogRedirectionStrategy( + LogRedirectionStrategy logRedirectionStrategy) { + _globalLogRedirectionStrategy = logRedirectionStrategy; + } + + /// Returns the number of messages that are not transmitted to the + /// Flutter callbacks yet for this session. + static Future messagesInTransmit(int sessionId) async { + try { + await init(); + return _platform.ffmpegKitConfigMessagesInTransmit(sessionId); + } on PlatformException catch (e, stack) { + print("Plugin messagesInTransmit error: ${e.message}"); + return Future.error("messagesInTransmit failed.", stack); + } + } + + /// Returns the string representation of the SessionState provided. + static String sessionStateToString(SessionState state) { + switch (state) { + case SessionState.created: + return "CREATED"; + case SessionState.running: + return "RUNNING"; + case SessionState.failed: + return "FAILED"; + case SessionState.completed: + return "COMPLETED"; + } + } + + /// Parses [command] into arguments. Uses space character to split the + /// arguments. Supports single and double quote characters. + static List parseArguments(String command) { + final List argumentList = List.empty(growable: true); + StringBuffer currentArgument = new StringBuffer(); + + bool singleQuoteStarted = false; + bool doubleQuoteStarted = false; + + for (int i = 0; i < command.length; i++) { + int? previousChar; + if (i > 0) { + previousChar = command.codeUnitAt(i - 1); + } else { + previousChar = null; + } + final currentChar = command.codeUnitAt(i); + + if (currentChar == ' '.codeUnitAt(0)) { + if (singleQuoteStarted || doubleQuoteStarted) { + currentArgument.write(String.fromCharCode(currentChar)); + } else if (currentArgument.length > 0) { + argumentList.add(currentArgument.toString()); + currentArgument = new StringBuffer(); + } + } else if (currentChar == '\''.codeUnitAt(0) && + (previousChar == null || previousChar != '\\'.codeUnitAt(0))) { + if (singleQuoteStarted) { + singleQuoteStarted = false; + } else if (doubleQuoteStarted) { + currentArgument.write(String.fromCharCode(currentChar)); + } else { + singleQuoteStarted = true; + } + } else if (currentChar == '\"'.codeUnitAt(0) && + (previousChar == null || previousChar != '\\'.codeUnitAt(0))) { + if (doubleQuoteStarted) { + doubleQuoteStarted = false; + } else if (singleQuoteStarted) { + currentArgument.write(String.fromCharCode(currentChar)); + } else { + doubleQuoteStarted = true; + } + } else { + currentArgument.write(String.fromCharCode(currentChar)); + } + } + + if (currentArgument.length > 0) { + argumentList.add(currentArgument.toString()); + } + + return argumentList; + } + + /// Concatenates arguments into a string adding a space character between + /// two arguments. + static String argumentsToString(List? arguments) { + if (arguments == null) { + return "null"; + } + + final StringBuffer stringBuffer = new StringBuffer(); + for (int i = 0; i < arguments.length; i++) { + if (i > 0) { + stringBuffer.write(" "); + } + stringBuffer.write(arguments[i]); + } + + return stringBuffer.toString(); + } + + // THE FOLLOWING METHODS ARE FLUTTER SPECIFIC + + /// Enables logs. + static Future enableLogs() async { + try { + await init(); + return _platform.ffmpegKitConfigEnableLogs(); + } on PlatformException catch (e, stack) { + print("Plugin enableLogs error: ${e.message}"); + return Future.error("enableLogs failed.", stack); + } + } + + /// Disable logs. + static Future disableLogs() async { + try { + await init(); + return _platform.ffmpegKitConfigDisableLogs(); + } on PlatformException catch (e, stack) { + print("Plugin disableLogs error: ${e.message}"); + return Future.error("disableLogs failed.", stack); + } + } + + /// Enables statistics. + static Future enableStatistics() async { + try { + await init(); + return _platform.ffmpegKitConfigEnableStatistics(); + } on PlatformException catch (e, stack) { + print("Plugin enableStatistics error: ${e.message}"); + return Future.error("enableStatistics failed.", stack); + } + } + + /// Disables statistics. + static Future disableStatistics() async { + try { + await init(); + return _platform.ffmpegKitConfigDisableStatistics(); + } on PlatformException catch (e, stack) { + print("Plugin disableStatistics error: ${e.message}"); + return Future.error("disableStatistics failed.", stack); + } + } + + /// Returns the platform name the library is loaded on. + static Future getPlatform() async { + try { + await init(); + return _platform.ffmpegKitConfigGetPlatform(); + } on PlatformException catch (e, stack) { + print("Plugin getPlatform error: ${e.message}"); + return Future.error("getPlatform failed.", stack); + } + } + + /// Writes [inputPath] to [pipePath]. + static Future writeToPipe(String inputPath, String pipePath) async { + try { + await init(); + return _platform.ffmpegKitConfigWriteToPipe(inputPath, pipePath); + } on PlatformException catch (e, stack) { + print("Plugin writeToPipe error: ${e.message}"); + return Future.error("writeToPipe failed.", stack); + } + } + + /// Displays the native file dialog to select a file in read mode. If a file + /// is selected then this method returns the Structured Access Framework Uri + /// ("content:…") for that file. + /// + /// Note that this method is Android only. It will fail if called on other + /// platforms. + static Future selectDocumentForRead( + [String? type = null, List? extraTypes = null]) async { + try { + await init(); + return _platform.ffmpegKitConfigSelectDocumentForRead(type, extraTypes); + } on PlatformException catch (e, stack) { + print("Plugin selectDocumentForRead error: ${e.message}"); + return Future.error("selectDocumentForRead failed.", stack); + } + } + + /// Displays the native file dialog to select a file in write mode. If a file + /// is selected then this method returns the Structured Access Framework Uri + /// ("content:…") for that file. + /// + /// Note that this method is Android only. It will fail if called on other + /// platforms. + static Future selectDocumentForWrite( + [String? title = null, + String? type = null, + List? extraTypes = null]) async { + try { + await init(); + return _platform.ffmpegKitConfigSelectDocumentForWrite( + title, type, extraTypes); + } on PlatformException catch (e, stack) { + print("Plugin selectDocumentForWrite error: ${e.message}"); + return Future.error("selectDocumentForWrite failed.", stack); + } + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_session.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_session.dart new file mode 100644 index 0000000..04280ab --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_session.dart @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2019-2022 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart'; +import 'package:flutter/services.dart'; + +import 'abstract_session.dart'; +import 'ffmpeg_kit_config.dart'; +import 'ffmpeg_session_complete_callback.dart'; +import 'log_callback.dart'; +import 'log_redirection_strategy.dart'; +import 'src/ffmpeg_kit_factory.dart'; +import 'statistics.dart'; +import 'statistics_callback.dart'; + +/// An FFmpeg session. +class FFmpegSession extends AbstractSession { + /// Creates a new FFmpeg session with [argumentsArray]. + static Future create(List argumentsArray, + [FFmpegSessionCompleteCallback? completeCallback = null, + LogCallback? logCallback = null, + StatisticsCallback? statisticsCallback = null, + LogRedirectionStrategy? logRedirectionStrategy = null]) async { + final session = await AbstractSession.createFFmpegSession( + argumentsArray, logRedirectionStrategy); + final sessionId = session.getSessionId(); + + FFmpegKitFactory.setFFmpegSessionCompleteCallback( + sessionId, completeCallback); + FFmpegKitFactory.setLogCallback(sessionId, logCallback); + FFmpegKitFactory.setStatisticsCallback(sessionId, statisticsCallback); + + return session; + } + + /// Returns the session specific statistics callback. + StatisticsCallback? getStatisticsCallback() => + FFmpegKitFactory.getStatisticsCallback(this.getSessionId()); + + /// Returns the session specific complete callback. + FFmpegSessionCompleteCallback? getCompleteCallback() => + FFmpegKitFactory.getFFmpegSessionCompleteCallback(this.getSessionId()); + + /// Returns all statistics entries generated for this session. If there are + /// asynchronous statistics that are not delivered yet, this method waits for + /// them until [waitTimeout]. + Future> getAllStatistics([int? waitTimeout]) async { + try { + await FFmpegKitConfig.init(); + return FFmpegKitPlatform.instance + .ffmpegSessionGetAllStatistics(this.getSessionId(), waitTimeout) + .then((allStatistics) { + if (allStatistics == null) { + return List.empty(); + } else { + return allStatistics + .map((dynamic statisticsObject) => + FFmpegKitFactory.mapToStatistics( + statisticsObject as Map)) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getAllStatistics error: ${e.message}"); + return Future.error("getAllStatistics failed.", stack); + } + } + + /// Returns all statistics entries delivered for this session. Note that if + /// there are asynchronous statistics that are not delivered yet, this method + /// will not wait for them and will return immediately. + Future> getStatistics() async { + try { + await FFmpegKitConfig.init(); + return FFmpegKitPlatform.instance + .ffmpegSessionGetStatistics(this.getSessionId()) + .then((statistics) { + if (statistics == null) { + return List.empty(); + } else { + return statistics + .map((dynamic statisticsObject) => + FFmpegKitFactory.mapToStatistics( + statisticsObject as Map)) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getStatistics error: ${e.message}"); + return Future.error("getStatistics failed.", stack); + } + } + + /// Returns the last received statistics entry. + Future getLastReceivedStatistics() async => + this.getStatistics().then((statistics) { + if (statistics.isNotEmpty) { + return statistics[statistics.length - 1]; + } else { + return null; + } + }); + + bool isFFmpeg() => true; + + bool isFFprobe() => false; + + bool isMediaInformation() => false; +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_session_complete_callback.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_session_complete_callback.dart new file mode 100644 index 0000000..4e8a40c --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffmpeg_session_complete_callback.dart @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'ffmpeg_session.dart'; + +/// Callback function that is invoked when an asynchronous FFmpeg session has +/// ended. Session has either SessionState.completed or SessionState.failed +/// state when the callback is invoked. +/// If it has SessionState.completed state, "ReturnCode" should be checked to +/// see the execution result. +/// If "getState" returns SessionState.failed then "getFailStackTrace" should +/// be used to get the failure reason. +typedef FFmpegSessionCompleteCallback = void Function(FFmpegSession session); diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_kit.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_kit.dart new file mode 100644 index 0000000..f0a92ab --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_kit.dart @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2019-2022 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart'; +import 'package:flutter/services.dart'; + +import 'ffmpeg_kit_config.dart'; +import 'ffprobe_session.dart'; +import 'ffprobe_session_complete_callback.dart'; +import 'log_callback.dart'; +import 'media_information_session.dart'; +import 'media_information_session_complete_callback.dart'; +import 'src/ffmpeg_kit_factory.dart'; + +/// Main class to run "FFprobe" commands. +class FFprobeKit { + static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance; + + /// Synchronously executes FFprobe command provided. Space character is used + /// to split command into arguments. You can use single or double quote + /// characters to specify arguments inside your command. + static Future execute(String command) async => + FFprobeKit.executeWithArguments(FFmpegKitConfig.parseArguments(command)); + + /// Synchronously executes FFprobe with arguments provided. + static Future executeWithArguments( + List commandArguments) async { + final session = + await FFprobeSession.create(commandArguments, null, null, null); + + await FFmpegKitConfig.ffprobeExecute(session); + + return session; + } + + /// Starts an asynchronous FFprobe execution for the given command. + /// Space character is used to split the command + /// into arguments. You can use single or double quote characters + /// to specify arguments inside your command. + /// + /// Note that this method returns immediately and does not wait the + /// execution to complete. You must use an + /// [FFprobeSessionCompleteCallback] if you want to be notified + /// about the result. + static Future executeAsync(String command, + [FFprobeSessionCompleteCallback? completeCallback = null, + LogCallback? logCallback = null]) async => + FFprobeKit.executeWithArgumentsAsync( + FFmpegKitConfig.parseArguments(command), + completeCallback, + logCallback); + + /// Starts an asynchronous FFprobe execution with arguments provided. + /// + /// Note that this method returns immediately and does not wait the + /// execution to complete. You must use an + /// [FFprobeSessionCompleteCallback] if you want to be notified + /// about the result. + static Future executeWithArgumentsAsync( + List commandArguments, + [FFprobeSessionCompleteCallback? completeCallback = null, + LogCallback? logCallback = null]) async { + final session = await FFprobeSession.create( + commandArguments, completeCallback, logCallback, null); + + await FFmpegKitConfig.asyncFFprobeExecute(session); + + return session; + } + + /// Extracts media information for the file specified with path. + static Future getMediaInformation(String path, + [int? waitTimeout = null]) async { + final commandArguments = [ + "-v", + "error", + "-hide_banner", + "-print_format", + "json", + "-show_format", + "-show_streams", + "-show_chapters", + "-i", + path + ]; + return FFprobeKit.getMediaInformationFromCommandArguments( + commandArguments, waitTimeout); + } + + /// Extracts media information using the command provided. The command + /// passed to this method must generate the output in JSON format in order + /// to successfully extract media information from it. + static Future getMediaInformationFromCommand( + String command, + [int? waitTimeout = null]) async => + FFprobeKit.getMediaInformationFromCommandArguments( + FFmpegKitConfig.parseArguments(command), waitTimeout); + + /// Extracts media information using the command arguments provided. The + /// command passed to this method must generate the output in JSON format in + /// order to successfully extract media information from it. + static Future + getMediaInformationFromCommandArguments(List commandArguments, + [int? waitTimeout = null]) async { + final session = + await MediaInformationSession.create(commandArguments, null, null); + + await FFmpegKitConfig.getMediaInformationExecute(session, waitTimeout); + + final mediaInformation = await _platform + .mediaInformationSessionGetMediaInformation(session.getSessionId()) + .then(FFmpegKitFactory.mapToNullableMediaInformation); + if (mediaInformation != null) { + session.setMediaInformation(mediaInformation); + } + + return session; + } + + /// Starts an asynchronous FFprobe execution to extract the media information + /// for the specified file. + /// + /// Note that this method returns immediately and does not wait the execution + /// to complete. You must use an + /// [MediaInformationSessionCompleteCallback] if you want to be notified about + /// the result. + static Future getMediaInformationAsync(String path, + [MediaInformationSessionCompleteCallback? completeCallback = null, + LogCallback? logCallback = null, + int? waitTimeout = null]) async { + final commandArguments = [ + "-v", + "error", + "-hide_banner", + "-print_format", + "json", + "-show_format", + "-show_streams", + "-show_chapters", + "-i", + path + ]; + return FFprobeKit.getMediaInformationFromCommandArgumentsAsync( + commandArguments, completeCallback, logCallback, waitTimeout); + } + + /// Starts an asynchronous FFprobe execution to extract media information + /// using a command. The command passed to + /// this method must generate the output in JSON format in order + /// to successfully extract media information from it. + /// + /// Note that this method returns immediately and does not wait the + /// execution to complete. You must use an + /// [MediaInformationSessionCompleteCallback] if you want to be notified + /// about the result. + static Future getMediaInformationFromCommandAsync( + String command, + [MediaInformationSessionCompleteCallback? completeCallback = null, + LogCallback? logCallback = null, + int? waitTimeout = null]) async => + FFprobeKit.getMediaInformationFromCommandArgumentsAsync( + FFmpegKitConfig.parseArguments(command), + completeCallback, + logCallback, + waitTimeout); + + /// Starts an asynchronous FFprobe execution to extract media information + /// using command arguments. The command passed to this method must generate + /// the output in JSON format in order to successfully extract media + /// information from it. + /// + /// Note that this method returns immediately and does not wait the execution + /// to complete. You must use an [MediaInformationSessionCompleteCallback] + /// if you want to be + /// notified about the result. + static Future + getMediaInformationFromCommandArgumentsAsync( + List commandArguments, + [MediaInformationSessionCompleteCallback? completeCallback = null, + LogCallback? logCallback = null, + int? waitTimeout = null]) async { + final session = await MediaInformationSession.create( + commandArguments, completeCallback, logCallback); + + await FFmpegKitConfig.asyncGetMediaInformationExecute(session, waitTimeout); + + final mediaInformation = await _platform + .mediaInformationSessionGetMediaInformation(session.getSessionId()) + .then(FFmpegKitFactory.mapToNullableMediaInformation); + if (mediaInformation != null) { + session.setMediaInformation(mediaInformation); + } + + return session; + } + + /// Lists all FFprobe sessions in the session history. + static Future> listFFprobeSessions() async { + try { + await FFmpegKitConfig.init(); + return _platform.ffprobeKitListFFprobeSessions().then((sessions) { + if (sessions == null) { + return List.empty(); + } else { + return sessions + .map((dynamic sessionObject) => FFmpegKitFactory.mapToSession( + sessionObject as Map)) + .map((session) => session as FFprobeSession) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin listFFprobeSessions error: ${e.message}"); + return Future.error("listFFprobeSessions failed.", stack); + } + } + + /// Lists all MediaInformation sessions in the session history. + static Future> + listMediaInformationSessions() async { + try { + await FFmpegKitConfig.init(); + return _platform + .ffprobeKitListMediaInformationSessions() + .then((sessions) { + if (sessions == null) { + return List.empty(); + } else { + return sessions + .map((dynamic sessionObject) => FFmpegKitFactory.mapToSession( + sessionObject as Map)) + .map((session) => session as MediaInformationSession) + .toList(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin listMediaInformationSessions error: ${e.message}"); + return Future.error("listMediaInformationSessions failed.", stack); + } + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_session.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_session.dart new file mode 100644 index 0000000..42e826d --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_session.dart @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2022 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'abstract_session.dart'; +import 'ffprobe_session_complete_callback.dart'; +import 'log_callback.dart'; +import 'log_redirection_strategy.dart'; +import 'src/ffmpeg_kit_factory.dart'; + +/// An FFprobe session. +class FFprobeSession extends AbstractSession { + /// Creates a new FFprobe session with [argumentsArray]. + static Future create(List argumentsArray, + [FFprobeSessionCompleteCallback? completeCallback = null, + LogCallback? logCallback = null, + LogRedirectionStrategy? logRedirectionStrategy = null]) async { + final session = await AbstractSession.createFFprobeSession( + argumentsArray, logRedirectionStrategy); + final sessionId = session.getSessionId(); + + FFmpegKitFactory.setFFprobeSessionCompleteCallback( + sessionId, completeCallback); + FFmpegKitFactory.setLogCallback(sessionId, logCallback); + + return session; + } + + /// Returns the session specific complete callback. + FFprobeSessionCompleteCallback? getCompleteCallback() => + FFmpegKitFactory.getFFprobeSessionCompleteCallback(this.getSessionId()); + + bool isFFmpeg() => false; + + bool isFFprobe() => true; + + bool isMediaInformation() => false; +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_session_complete_callback.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_session_complete_callback.dart new file mode 100644 index 0000000..cfdca17 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/ffprobe_session_complete_callback.dart @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'ffprobe_session.dart'; + +/// Callback function that is invoked when an asynchronous FFprobe session has +/// ended. Session has either SessionState.completed or SessionState.failed +/// state when the callback is invoked. +/// If it has SessionState.completed state, "ReturnCode" should be checked to +/// see the execution result. +/// If "getState" returns SessionState.failed then "getFailStackTrace" should +/// be used to get the failure reason. +typedef FFprobeSessionCompleteCallback = void Function(FFprobeSession session); diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/level.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/level.dart new file mode 100644 index 0000000..9ef3aa8 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/level.dart @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +/// Defines log levels. +class Level { + /// This log level is defined by FFmpegKit. It is used to specify logs + /// printed to stderr by FFmpeg. Logs that has this level are not filtered + /// and always redirected. + static const int avLogStderr = -16; + + /// Print no output. + static const int avLogQuiet = -8; + + /// Something went really wrong and we will crash now. + static const int avLogPanic = 0; + + /// Something went wrong and recovery is not possible. + /// For example, no header was found for a format which depends + /// on headers or an illegal combination of parameters is used. + static const int avLogFatal = 8; + + /// Something went wrong and cannot losslessly be recovered. + /// However, not all future data is affected. + static const int avLogError = 16; + + /// Something somehow does not look correct. This may or may not + /// lead to problems. An example would be the use of '-vstrict -2'. + static const int avLogWarning = 24; + + /// int Standard information. + static const int avLogInfo = 32; + + /// Detailed information. + static const int avLogVerbose = 40; + + /// Stuff which is only useful for libav* developers. + static const int avLogDebug = 48; + + /// Extremely verbose debugging, useful for libav* development. + static const int avLogTrace = 56; + + /// Returns log level string from int + static String levelToString(int level) { + switch (level) { + case Level.avLogTrace: + return "TRACE"; + case Level.avLogDebug: + return "DEBUG"; + case Level.avLogVerbose: + return "VERBOSE"; + case Level.avLogInfo: + return "INFO"; + case Level.avLogWarning: + return "WARNING"; + case Level.avLogError: + return "ERROR"; + case Level.avLogFatal: + return "FATAL"; + case Level.avLogPanic: + return "PANIC"; + case Level.avLogStderr: + return "STDERR"; + case Level.avLogQuiet: + default: + return ""; + } + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/log.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/log.dart new file mode 100644 index 0000000..c9c4743 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/log.dart @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +/// Log entry for an FFmpegKit session. +class Log { + int _sessionId; + int _level; + String _message; + + Log(this._sessionId, this._level, this._message); + + int getSessionId() => _sessionId; + + int getLevel() => _level; + + String getMessage() => _message; +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/log_callback.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/log_callback.dart new file mode 100644 index 0000000..fc43d18 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/log_callback.dart @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'log.dart'; + +/// Callback function that receives logs generated for "FFmpegKit" sessions. +typedef LogCallback = void Function(Log log); diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/log_redirection_strategy.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/log_redirection_strategy.dart new file mode 100644 index 0000000..259119e --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/log_redirection_strategy.dart @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +enum LogRedirectionStrategy { + alwaysPrintLogs, + printLogsWhenNoCallbacksDefined, + printLogsWhenGlobalCallbackNotDefined, + printLogsWhenSessionCallbackNotDefined, + neverPrintLogs +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information.dart new file mode 100644 index 0000000..1abb89e --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information.dart @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2019-2022 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'chapter.dart'; +import 'stream_information.dart'; + +/// Media information class. +class MediaInformation { + static const keyFormatProperties = "format"; + static const keyFilename = "filename"; + static const keyFormat = "format_name"; + static const keyFormatLong = "format_long_name"; + static const keyStartTime = "start_time"; + static const keyDuration = "duration"; + static const keySize = "size"; + static const keyBitRate = "bit_rate"; + static const keyTags = "tags"; + + Map? _allProperties; + + /// Creates a new [MediaInformation] instance + MediaInformation(this._allProperties); + + /// Returns file name. + String? getFilename() => + this.getStringFormatProperty(MediaInformation.keyFilename); + + /// Returns format. + String? getFormat() => + this.getStringFormatProperty(MediaInformation.keyFormat); + + /// Returns long format. + String? getLongFormat() => + this.getStringFormatProperty(MediaInformation.keyFormatLong); + + /// Returns duration. + String? getDuration() => + this.getStringFormatProperty(MediaInformation.keyDuration); + + /// Returns start time. + String? getStartTime() => + this.getStringFormatProperty(MediaInformation.keyStartTime); + + /// Returns size. + String? getSize() => this.getStringFormatProperty(MediaInformation.keySize); + + /// Returns bitrate. + String? getBitrate() => + this.getStringFormatProperty(MediaInformation.keyBitRate); + + /// Returns all tags. + Map? getTags() => + this.getFormatProperty(StreamInformation.keyTags); + + /// Returns the property associated with the key. + String? getStringProperty(String key) => this.getAllProperties()?[key]; + + /// Returns the property associated with the key. + num? getNumberProperty(String key) => this.getAllProperties()?[key]; + + /// Returns the property associated with the key. + dynamic getProperty(String key) => this.getAllProperties()?[key]; + + /// Returns the format property associated with the key. + String? getStringFormatProperty(String key) => + this.getFormatProperties()?[key]; + + /// Returns the format property associated with the key. + num? getNumberFormatProperty(String key) => this.getFormatProperties()?[key]; + + /// Returns the format property associated with the key. + dynamic getFormatProperty(String key) => this.getFormatProperties()?[key]; + + /// Returns all streams found as a list. + List getStreams() { + final List list = + List.empty(growable: true); + + dynamic createStreamInformation(Map streamProperties) => + list.add(new StreamInformation(streamProperties)); + + this._allProperties?["streams"]?.forEach((Object? stream) { + createStreamInformation(stream as Map); + }); + + return list; + } + + /// Returns all chapters found as a list. + List getChapters() { + final List list = List.empty(growable: true); + + dynamic createChapter(Map chapterProperties) => + list.add(new Chapter(chapterProperties)); + + this._allProperties?["chapters"]?.forEach((Object? chapter) { + createChapter(chapter as Map); + }); + + return list; + } + + /// Returns all format properties found. + Map? getFormatProperties() => + this._allProperties?[keyFormatProperties]; + + /// Returns all properties found, including stream properties. + Map? getAllProperties() => this._allProperties; +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_json_parser.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_json_parser.dart new file mode 100644 index 0000000..09725d6 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_json_parser.dart @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart'; +import 'package:flutter/services.dart'; + +import 'ffmpeg_kit_config.dart'; +import 'media_information.dart'; + +/// A parser that constructs "MediaInformation" from FFprobe's json output. +class MediaInformationJsonParser { + static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance; + + /// Extracts MediaInformation from the given FFprobe json output. Note that + /// this method does not fail as [fromWithError] does and returns null on + /// error. + static Future from(String ffprobeJsonOutput) async { + try { + await FFmpegKitConfig.init(); + return _platform + .mediaInformationJsonParserFrom(ffprobeJsonOutput) + .then((properties) { + if (properties == null || properties.isEmpty) { + return null; + } else { + return new MediaInformation(properties); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin from error: ${e.message}"); + return Future.error("from failed.", stack); + } + } + + /// Extracts MediaInformation from the given FFprobe json output. + static Future fromWithError( + String ffprobeJsonOutput) async { + try { + await FFmpegKitConfig.init(); + return _platform + .mediaInformationJsonParserFromWithError(ffprobeJsonOutput) + .then(MediaInformation.new); + } on PlatformException catch (e, stack) { + print("Plugin fromWithError error: ${e.message}"); + return Future.error("fromWithError failed.", stack); + } + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_session.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_session.dart new file mode 100644 index 0000000..472fa1b --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_session.dart @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019-2022 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'abstract_session.dart'; +import 'log_callback.dart'; +import 'media_information.dart'; +import 'media_information_session_complete_callback.dart'; +import 'src/ffmpeg_kit_factory.dart'; + +/// A custom FFprobe session, which produces a "MediaInformation" object +/// using the FFprobe output. +class MediaInformationSession extends AbstractSession { + MediaInformation? _mediaInformation; + + /// Creates a new MediaInformation session with [argumentsArray]. + static Future create(List argumentsArray, + [MediaInformationSessionCompleteCallback? completeCallback = null, + LogCallback? logCallback = null]) async { + final session = + await AbstractSession.createMediaInformationSession(argumentsArray); + final sessionId = session.getSessionId(); + + FFmpegKitFactory.setMediaInformationSessionCompleteCallback( + sessionId, completeCallback); + FFmpegKitFactory.setLogCallback(sessionId, logCallback); + + return session; + } + + /// Returns the media information extracted in this session. + MediaInformation? getMediaInformation() => this._mediaInformation; + + /// Sets the media information extracted in this session. + void setMediaInformation(MediaInformation? mediaInformation) { + this._mediaInformation = mediaInformation; + } + + /// Returns the session specific complete callback. + MediaInformationSessionCompleteCallback? getCompleteCallback() => + FFmpegKitFactory.getMediaInformationSessionCompleteCallback( + this.getSessionId()); + + bool isFFmpeg() => false; + + bool isFFprobe() => false; + + bool isMediaInformation() => true; +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_session_complete_callback.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_session_complete_callback.dart new file mode 100644 index 0000000..213900f --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/media_information_session_complete_callback.dart @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'media_information_session.dart'; + +/// Callback function that is invoked when an asynchronous MediaInformation +/// session has ended. Session has either SessionState.completed or +/// SessionState.failed state when the callback is invoked. +/// If it has SessionState.completed state, "ReturnCode" should be checked to +/// see the execution result. +/// If "getState" returns SessionState.failed then "getFailStackTrace" should +/// be used to get the failure reason. +typedef MediaInformationSessionCompleteCallback = void Function( + MediaInformationSession session); diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/packages.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/packages.dart new file mode 100644 index 0000000..f465cef --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/packages.dart @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart'; +import 'package:flutter/services.dart'; + +import 'ffmpeg_kit_config.dart'; + +/// Helper class to extract binary package information. +class Packages { + static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance; + + /// Returns the FFmpegKit Flutter binary package name. + static Future getPackageName() async { + try { + await FFmpegKitConfig.init(); + return _platform.getPackageName(); + } on PlatformException catch (e, stack) { + print("Plugin getPackageName error: ${e.message}"); + return Future.error("getPackageName failed.", stack); + } + } + + /// Returns enabled external libraries by FFmpeg. + static Future> getExternalLibraries() async { + try { + await FFmpegKitConfig.init(); + return _platform.getExternalLibraries().then((externalLibraries) { + if (externalLibraries == null) { + return List.empty(); + } else { + return externalLibraries.cast(); + } + }); + } on PlatformException catch (e, stack) { + print("Plugin getExternalLibraries error: ${e.message}"); + return Future.error("getExternalLibraries failed.", stack); + } + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/return_code.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/return_code.dart new file mode 100644 index 0000000..9a34ec5 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/return_code.dart @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +class ReturnCode { + static const int success = 0; + static const int cancel = 255; + + int _value; + + ReturnCode(this._value); + + static bool isSuccess(ReturnCode? returnCode) => + returnCode?.getValue() == ReturnCode.success; + + static bool isCancel(ReturnCode? returnCode) => + returnCode?.getValue() == ReturnCode.cancel; + + int getValue() => this._value; + + bool isValueSuccess() => this._value == ReturnCode.success; + + bool isValueError() => + (this._value != ReturnCode.success) && (this._value != ReturnCode.cancel); + + bool isValueCancel() => this._value == ReturnCode.cancel; + + String toString() => this._value.toString(); +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/session.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/session.dart new file mode 100644 index 0000000..a4796d8 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/session.dart @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'log.dart'; +import 'log_callback.dart'; +import 'log_redirection_strategy.dart'; +import 'return_code.dart'; +import 'session_state.dart'; + +/// Common interface for all "FFmpegKit" sessions. +abstract class Session { + /// Returns the session specific log callback. + LogCallback? getLogCallback(); + + /// Returns the session identifier. + int? getSessionId(); + + /// Returns session create time. + DateTime? getCreateTime(); + + /// Returns session start time. + DateTime? getStartTime(); + + /// Returns session end time. + Future getEndTime(); + + /// Returns time taken to execute this session in milliseconds or zero (0) + /// if the session is not over yet. + Future getDuration(); + + /// Returns command arguments as an array. + List? getArguments(); + + /// Returns command arguments as a concatenated string. + String? getCommand(); + + /// Returns all log entries generated for this session. If there are + /// asynchronous logs that are not delivered yet, this method waits for + /// them until [waitTimeout]. + Future> getAllLogs([int? waitTimeout = null]); + + /// Returns all log entries delivered for this session. Note that if there + /// are asynchronous logs that are not delivered yet, this method + /// will not wait for them and will return immediately. + Future> getLogs(); + + /// Returns all log entries generated for this session as a concatenated + /// string. If there are asynchronous logs that are not delivered yet, + /// this method waits for them until [waitTimeout]. + Future getAllLogsAsString([int? waitTimeout = null]); + + /// Returns all log entries delivered for this session as a concatenated + /// string. Note that if there are asynchronous logs that are not + /// delivered yet, this method will not wait for them and will return + /// immediately. + Future getLogsAsString(); + + /// Returns the log output generated while running the session. + Future getOutput(); + + /// Returns the state of the session. + Future getState(); + + /// Returns the return code for this session. Note that return code is only + /// set for sessions that end with COMPLETED state. If a session is not + /// started, still running or failed then this method returns null. + Future getReturnCode(); + + /// Returns the stack trace of the exception received while executing this + /// session. + /// + /// The stack trace is only set for sessions that end with FAILED state. For + /// sessions that has COMPLETED state this method returns null. + Future getFailStackTrace(); + + /// Returns session specific log redirection strategy. + LogRedirectionStrategy? getLogRedirectionStrategy(); + + /// Returns whether there are still asynchronous messages being transmitted + /// for this session or not. + Future thereAreAsynchronousMessagesInTransmit(); + + /// Returns whether it is an "FFmpeg" session or not. + bool isFFmpeg(); + + /// Returns whether it is an "FFprobe" session or not. + bool isFFprobe(); + + /// Returns whether it is an "MediaInformation" session or not. + bool isMediaInformation(); + + /// Cancels running the session. + Future cancel(); +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/session_state.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/session_state.dart new file mode 100644 index 0000000..74ccf0f --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/session_state.dart @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +enum SessionState { created, running, failed, completed } diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/signal.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/signal.dart new file mode 100644 index 0000000..93f9ef3 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/signal.dart @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +/// Signals handled by FFmpegKit library. +enum Signal { sigInt, sigQuit, sigPipe, sigTerm, sigXCpu } diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/src/ffmpeg_kit_factory.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/src/ffmpeg_kit_factory.dart new file mode 100644 index 0000000..e3c0e36 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/src/ffmpeg_kit_factory.dart @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2019-2022 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import '../abstract_session.dart'; +import '../ffmpeg_session_complete_callback.dart'; +import '../ffprobe_session_complete_callback.dart'; +import '../log.dart'; +import '../log_callback.dart'; +import '../log_redirection_strategy.dart'; +import '../media_information.dart'; +import '../media_information_session_complete_callback.dart'; +import '../session.dart'; +import '../statistics.dart'; +import '../statistics_callback.dart'; + +final ffmpegSessionCompleteCallbackMap = + new Map(); +final ffprobeSessionCompleteCallbackMap = + new Map(); +final mediaInformationSessionCompleteCallbackMap = + new Map(); +final logCallbackMap = new Map(); +final statisticsCallbackMap = new Map(); +final logRedirectionStrategyMap = new Map(); + +class FFmpegKitFactory { + static LogCallback? _logCallback; + static StatisticsCallback? _statisticsCallback; + static FFmpegSessionCompleteCallback? _ffmpegSessionCompleteCallback; + static FFprobeSessionCompleteCallback? _ffprobeSessionCompleteCallback; + static MediaInformationSessionCompleteCallback? + _mediaInformationSessionCompleteCallback; + + static Statistics mapToStatistics(Map statisticsMap) => + new Statistics( + statisticsMap["sessionId"].toInt(), + statisticsMap["videoFrameNumber"].toInt(), + statisticsMap["videoFps"], + statisticsMap["videoQuality"], + statisticsMap["size"].toInt(), + statisticsMap["time"].toInt(), + statisticsMap["bitrate"], + statisticsMap["speed"]); + + static Log mapToLog(Map logMap) => + new Log(logMap["sessionId"], logMap["level"], logMap["message"]); + + static Session mapToSession(Map sessionMap) { + switch (sessionMap["type"]) { + case 2: + return AbstractSession.createFFprobeSessionFromMap(sessionMap); + case 3: + return AbstractSession.createMediaInformationSessionFromMap(sessionMap); + case 1: + default: + return AbstractSession.createFFmpegSessionFromMap(sessionMap); + } + } + + static Session? mapToNullableSession(Map? sessionMap) { + if (sessionMap != null) { + switch (sessionMap["type"]) { + case 2: + return AbstractSession.createFFprobeSessionFromMap(sessionMap); + case 3: + return AbstractSession.createMediaInformationSessionFromMap( + sessionMap); + case 1: + default: + return AbstractSession.createFFmpegSessionFromMap(sessionMap); + } + } else { + return null; + } + } + + static MediaInformation? mapToNullableMediaInformation( + Map? mediaInformationMap) { + if (mediaInformationMap != null) { + return new MediaInformation(mediaInformationMap); + } else { + return null; + } + } + + static String getVersion() => "8.0.0"; + + static LogRedirectionStrategy? getLogRedirectionStrategy(int? sessionId) => + logRedirectionStrategyMap[sessionId]; + + static void setLogRedirectionStrategy( + int? sessionId, LogRedirectionStrategy? logRedirectionStrategy) { + if (sessionId != null && logRedirectionStrategy != null) { + logRedirectionStrategyMap[sessionId] = logRedirectionStrategy; + } + } + + static LogCallback? getLogCallback(int? sessionId) => + logCallbackMap[sessionId]; + + static void setLogCallback(int? sessionId, LogCallback? logCallback) { + if (sessionId != null && logCallback != null) { + logCallbackMap[sessionId] = logCallback; + } + } + + static LogCallback? getGlobalLogCallback() => _logCallback; + + static void setGlobalLogCallback(LogCallback? logCallback) { + _logCallback = logCallback; + } + + static StatisticsCallback? getStatisticsCallback(int? sessionId) => + statisticsCallbackMap[sessionId]; + + static void setStatisticsCallback( + int? sessionId, StatisticsCallback? statisticsCallback) { + if (sessionId != null && statisticsCallback != null) { + statisticsCallbackMap[sessionId] = statisticsCallback; + } + } + + static StatisticsCallback? getGlobalStatisticsCallback() => + _statisticsCallback; + + static void setGlobalStatisticsCallback( + StatisticsCallback? statisticsCallback) { + _statisticsCallback = statisticsCallback; + } + + static FFmpegSessionCompleteCallback? getFFmpegSessionCompleteCallback( + int? sessionId) => + ffmpegSessionCompleteCallbackMap[sessionId]; + + static void setFFmpegSessionCompleteCallback( + int? sessionId, FFmpegSessionCompleteCallback? completeCallback) { + if (sessionId != null && completeCallback != null) { + ffmpegSessionCompleteCallbackMap[sessionId] = completeCallback; + } + } + + static FFmpegSessionCompleteCallback? + getGlobalFFmpegSessionCompleteCallback() => + _ffmpegSessionCompleteCallback; + + static void setGlobalFFmpegSessionCompleteCallback( + FFmpegSessionCompleteCallback? completeCallback) { + _ffmpegSessionCompleteCallback = completeCallback; + } + + static FFprobeSessionCompleteCallback? getFFprobeSessionCompleteCallback( + int? sessionId) => + ffprobeSessionCompleteCallbackMap[sessionId]; + + static void setFFprobeSessionCompleteCallback( + int? sessionId, FFprobeSessionCompleteCallback? completeCallback) { + if (sessionId != null && completeCallback != null) { + ffprobeSessionCompleteCallbackMap[sessionId] = completeCallback; + } + } + + static FFprobeSessionCompleteCallback? + getGlobalFFprobeSessionCompleteCallback() => + _ffprobeSessionCompleteCallback; + + static void setGlobalFFprobeSessionCompleteCallback( + FFprobeSessionCompleteCallback? completeCallback) { + _ffprobeSessionCompleteCallback = completeCallback; + } + + static MediaInformationSessionCompleteCallback? + getMediaInformationSessionCompleteCallback(int? sessionId) => + mediaInformationSessionCompleteCallbackMap[sessionId]; + + static void setMediaInformationSessionCompleteCallback(int? sessionId, + MediaInformationSessionCompleteCallback? completeCallback) { + if (sessionId != null && completeCallback != null) { + mediaInformationSessionCompleteCallbackMap[sessionId] = completeCallback; + } + } + + static MediaInformationSessionCompleteCallback? + getGlobalMediaInformationSessionCompleteCallback() => + _mediaInformationSessionCompleteCallback; + + static void setGlobalMediaInformationSessionCompleteCallback( + MediaInformationSessionCompleteCallback? completeCallback) { + _mediaInformationSessionCompleteCallback = completeCallback; + } + + static DateTime? validDate(int? time) { + if (time == null || time <= 0) { + return null; + } else { + return DateTime.fromMillisecondsSinceEpoch(time, isUtc: false); + } + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/src/ffmpeg_kit_flutter_initializer.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/src/ffmpeg_kit_flutter_initializer.dart new file mode 100644 index 0000000..4844762 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/src/ffmpeg_kit_flutter_initializer.dart @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'dart:io'; + +import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../arch_detect.dart'; +import '../ffmpeg_kit_config.dart'; +import '../ffmpeg_session.dart'; +import '../ffmpeg_session_complete_callback.dart'; +import '../ffprobe_session.dart'; +import '../ffprobe_session_complete_callback.dart'; +import '../level.dart'; +import '../log_callback.dart'; +import '../log_redirection_strategy.dart'; +import '../media_information_session.dart'; +import '../media_information_session_complete_callback.dart'; +import '../packages.dart'; +import '../session.dart'; +import '../statistics.dart'; +import '../statistics_callback.dart'; +import 'ffmpeg_kit_factory.dart'; + +class FFmpegKitInitializer { + static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance; + static const EventChannel _eventChannel = + const EventChannel('flutter.arthenica.com/ffmpeg_kit_event'); + + static FFmpegKitInitializer _instance = new FFmpegKitInitializer(); + + static bool _initialized = false; + + static Future initialize() async { + if (!_initialized) { + _initialized = true; + await _instance._initialize(); + } + return _initialized; + } + + void _onEvent(dynamic event) { + if (event is Map) { + final Map eventMap = event.cast(); + final Map? logEvent = + eventMap['FFmpegKitLogCallbackEvent']; + final Map? statisticsEvent = + eventMap['FFmpegKitStatisticsCallbackEvent']; + final Map? completeEvent = + eventMap['FFmpegKitCompleteCallbackEvent']; + + if (logEvent != null) { + _processLogCallbackEvent(logEvent); + } + + if (statisticsEvent != null) { + _processStatisticsCallbackEvent(statisticsEvent); + } + + if (completeEvent != null) { + _processCompleteCallbackEvent(completeEvent); + } + } + } + + void _onError(Object error) { + print('Event error: $error'); + } + + void _processLogCallbackEvent(Map event) { + final log = FFmpegKitFactory.mapToLog(event); + final int sessionId = event["sessionId"]; + final int level = event["level"]; + final String text = event["message"]; + final int activeLogLevel = FFmpegKitConfig.getLogLevel(); + var globalCallbackDefined = false; + var sessionCallbackDefined = false; + LogRedirectionStrategy activeLogRedirectionStrategy = + FFmpegKitConfig.getLogRedirectionStrategy(); + + // avLogStderr logs are always redirected + if ((activeLogLevel == Level.avLogQuiet && level != Level.avLogStderr) || + level > activeLogLevel) { + // LOG NEITHER PRINTED NOR FORWARDED + return; + } + + activeLogRedirectionStrategy = + FFmpegKitFactory.getLogRedirectionStrategy(sessionId) ?? + activeLogRedirectionStrategy; + final LogCallback? logCallback = FFmpegKitFactory.getLogCallback(sessionId); + + if (logCallback != null) { + sessionCallbackDefined = true; + + try { + // NOTIFY SESSION CALLBACK DEFINED + logCallback(log); + } on Exception catch (e, stack) { + print("Exception thrown inside session log callback. $e"); + print(stack); + } + } + + final globalLogCallbackFunction = FFmpegKitFactory.getGlobalLogCallback(); + if (globalLogCallbackFunction != null) { + globalCallbackDefined = true; + + try { + // NOTIFY GLOBAL CALLBACK DEFINED + globalLogCallbackFunction(log); + } on Exception catch (e, stack) { + print("Exception thrown inside global log callback. $e"); + print(stack); + } + } + + // EXECUTE THE LOG STRATEGY + switch (activeLogRedirectionStrategy) { + case LogRedirectionStrategy.neverPrintLogs: + { + return; + } + case LogRedirectionStrategy.printLogsWhenGlobalCallbackNotDefined: + { + if (globalCallbackDefined) { + return; + } + } + break; + case LogRedirectionStrategy.printLogsWhenSessionCallbackNotDefined: + { + if (sessionCallbackDefined) { + return; + } + } + break; + case LogRedirectionStrategy.printLogsWhenNoCallbacksDefined: + { + if (globalCallbackDefined || sessionCallbackDefined) { + return; + } + } + break; + case LogRedirectionStrategy.alwaysPrintLogs: + {} + break; + } + + // PRINT LOGS + switch (level) { + case Level.avLogQuiet: + { + // PRINT NO OUTPUT + } + break; + default: + { + stdout.write(text); + } + } + } + + void _processStatisticsCallbackEvent(Map event) { + final Statistics statistics = FFmpegKitFactory.mapToStatistics(event); + final int sessionId = event["sessionId"]; + + final StatisticsCallback? statisticsCallback = + FFmpegKitFactory.getStatisticsCallback(sessionId); + if (statisticsCallback != null) { + try { + // NOTIFY SESSION CALLBACK DEFINED + statisticsCallback(statistics); + } on Exception catch (e, stack) { + print("Exception thrown inside session statistics callback. $e"); + print(stack); + } + } + + final globalStatisticsCallbackFunction = + FFmpegKitFactory.getGlobalStatisticsCallback(); + if (globalStatisticsCallbackFunction != null) { + try { + // NOTIFY GLOBAL CALLBACK DEFINED + globalStatisticsCallbackFunction(statistics); + } on Exception catch (e, stack) { + print("Exception thrown inside global statistics callback. $e"); + print(stack); + } + } + } + + void _processCompleteCallbackEvent(Map event) { + final int sessionId = event["sessionId"]; + + FFmpegKitConfig.getSession(sessionId).then((Session? session) { + if (session != null) { + if (session.isFFmpeg()) { + final ffmpegSession = session as FFmpegSession; + final FFmpegSessionCompleteCallback? completeCallback = + ffmpegSession.getCompleteCallback(); + + if (completeCallback != null) { + try { + // NOTIFY SESSION CALLBACK DEFINED + completeCallback(ffmpegSession); + } on Exception catch (e, stack) { + print("Exception thrown inside session complete callback. $e"); + print(stack); + } + } + + final globalFFmpegSessionCompleteCallback = + FFmpegKitFactory.getGlobalFFmpegSessionCompleteCallback(); + if (globalFFmpegSessionCompleteCallback != null) { + try { + // NOTIFY GLOBAL CALLBACK DEFINED + globalFFmpegSessionCompleteCallback(ffmpegSession); + } on Exception catch (e, stack) { + print("Exception thrown inside global complete callback. $e"); + print(stack); + } + } + } else if (session.isFFprobe()) { + final ffprobeSession = session as FFprobeSession; + final FFprobeSessionCompleteCallback? completeCallback = + ffprobeSession.getCompleteCallback(); + + if (completeCallback != null) { + try { + // NOTIFY SESSION CALLBACK DEFINED + completeCallback(ffprobeSession); + } on Exception catch (e, stack) { + print("Exception thrown inside session complete callback. $e"); + print(stack); + } + } + + final globalFFprobeSessionCompleteCallback = + FFmpegKitFactory.getGlobalFFprobeSessionCompleteCallback(); + if (globalFFprobeSessionCompleteCallback != null) { + try { + // NOTIFY GLOBAL CALLBACK DEFINED + globalFFprobeSessionCompleteCallback(ffprobeSession); + } on Exception catch (e, stack) { + print("Exception thrown inside global complete callback. $e"); + print(stack); + } + } + } else if (session.isMediaInformation()) { + final mediaInformationSession = session as MediaInformationSession; + final MediaInformationSessionCompleteCallback? completeCallback = + mediaInformationSession.getCompleteCallback(); + + if (completeCallback != null) { + try { + // NOTIFY SESSION CALLBACK DEFINED + completeCallback(mediaInformationSession); + } on Exception catch (e, stack) { + print("Exception thrown inside session complete callback. $e"); + print(stack); + } + } + + final globalMediaInformationSessionCompleteCallback = FFmpegKitFactory + .getGlobalMediaInformationSessionCompleteCallback(); + if (globalMediaInformationSessionCompleteCallback != null) { + try { + // NOTIFY GLOBAL CALLBACK DEFINED + globalMediaInformationSessionCompleteCallback( + mediaInformationSession); + } on Exception catch (e, stack) { + print("Exception thrown inside global complete callback. $e"); + print(stack); + } + } + } + } + }); + } + + Future _getLogLevel() async { + try { + // const MethodChannel _channel = + // const MethodChannel('flutter.arthenica.com/ffmpeg_kit'); + // return await _channel.invokeMethod('getLogLevel'); + + return _platform.ffmpegKitFlutterInitializerGetLogLevel(); + } on PlatformException catch (e, stack) { + print("Plugin _getLogLevel error: ${e.message}"); + return Future.error("_getLogLevel failed.", stack); + } + } + + Future _initialize() async { + debugPrint("Loading ffmpeg-kit-flutter."); + + _eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError); + + final logLevel = await _getLogLevel(); + if (logLevel != null) { + FFmpegKitConfig.setLogLevel(logLevel); + } + + final version = FFmpegKitFactory.getVersion(); + final platform = await FFmpegKitConfig.getPlatform(); + final arch = await ArchDetect.getArch(); + final packageName = await Packages.getPackageName(); + await FFmpegKitConfig.enableRedirection(); + final isLTSPostfix = (await FFmpegKitConfig.isLTSBuild()) ? "-lts" : ""; + + final fullVersion = "$platform-$packageName-$arch-$version$isLTSPostfix"; + print("Loaded ffmpeg-kit-flutter-$fullVersion."); + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/statistics.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/statistics.dart new file mode 100644 index 0000000..9fbdcd2 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/statistics.dart @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +/// Statistics entry for an FFmpeg execute session. +class Statistics { + int _sessionId; + int _videoFrameNumber; + double _videoFps; + double _videoQuality; + int _size; + int _time; + double _bitrate; + double _speed; + + Statistics(this._sessionId, this._videoFrameNumber, this._videoFps, + this._videoQuality, this._size, this._time, this._bitrate, this._speed); + + int getSessionId() => this._sessionId; + + void setSessionId(int sessionId) { + this._sessionId = sessionId; + } + + int getVideoFrameNumber() => this._videoFrameNumber; + + void setVideoFrameNumber(int videoFrameNumber) { + this._videoFrameNumber = videoFrameNumber; + } + + double getVideoFps() => this._videoFps; + + void setVideoFps(double videoFps) { + this._videoFps = videoFps; + } + + double getVideoQuality() => this._videoQuality; + + void setVideoQuality(double videoQuality) { + this._videoQuality = videoQuality; + } + + int getSize() => this._size; + + void setSize(int size) { + this._size = size; + } + + int getTime() => this._time; + + void setTime(int time) { + this._time = time; + } + + double getBitrate() => this._bitrate; + + void setBitrate(double bitrate) { + this._bitrate = bitrate; + } + + double getSpeed() => this._speed; + + void setSpeed(double speed) { + this._speed = speed; + } +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/statistics_callback.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/statistics_callback.dart new file mode 100644 index 0000000..de988a6 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/statistics_callback.dart @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +import 'statistics.dart'; + +/// Callback function that receives statistics generated for "FFmpeg" sessions. +typedef StatisticsCallback = void Function(Statistics statistics); diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/lib/stream_information.dart b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/stream_information.dart new file mode 100644 index 0000000..0af0528 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/lib/stream_information.dart @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2019-2022 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +/// Stream information class. +class StreamInformation { + static const keyIndex = "index"; + static const keyType = "codec_type"; + static const keyCodec = "codec_name"; + static const keyCodecLong = "codec_long_name"; + static const keyFormat = "pix_fmt"; + static const keyWidth = "width"; + static const keyHeight = "height"; + static const keyBitRate = "bit_rate"; + static const keySampleRate = "sample_rate"; + static const keySampleFormat = "sample_fmt"; + static const keyChannelLayout = "channel_layout"; + static const keySampleAspectRatio = "sample_aspect_ratio"; + static const keyDisplayAspectRatio = "display_aspect_ratio"; + static const keyAverageFrameRate = "avg_frame_rate"; + static const keyRealFrameRate = "r_frame_rate"; + static const keyTimeBase = "time_base"; + static const keyCodecTimeBase = "codec_time_base"; + static const keyTags = "tags"; + + Map? _allProperties; + + /// Creates a new [StreamInformation] instance + StreamInformation(this._allProperties); + + /// Returns stream index. + int? getIndex() => this.getNumberProperty(keyIndex)?.toInt(); + + /// Returns stream type. + String? getType() => this.getStringProperty(StreamInformation.keyType); + + /// Returns stream codec. + String? getCodec() => this.getStringProperty(StreamInformation.keyCodec); + + /// Returns stream codec in long format. + String? getFullCodec() => + this.getStringProperty(StreamInformation.keyCodecLong); + + /// Returns stream format. + String? getFormat() => this.getStringProperty(StreamInformation.keyFormat); + + /// Returns width. + int? getWidth() => + this.getNumberProperty(StreamInformation.keyWidth)?.toInt(); + + /// Returns height. + int? getHeight() => + this.getNumberProperty(StreamInformation.keyHeight)?.toInt(); + + /// Returns bitrate. + String? getBitrate() => this.getStringProperty(StreamInformation.keyBitRate); + + /// Returns sample rate. + String? getSampleRate() => + this.getStringProperty(StreamInformation.keySampleRate); + + /// Returns sample format. + String? getSampleFormat() => + this.getStringProperty(StreamInformation.keySampleFormat); + + /// Returns channel layout. + String? getChannelLayout() => + this.getStringProperty(StreamInformation.keyChannelLayout); + + /// Returns sample aspect ratio. + String? getSampleAspectRatio() => + this.getStringProperty(StreamInformation.keySampleAspectRatio); + + /// Returns display aspect ratio. + String? getDisplayAspectRatio() => + this.getStringProperty(StreamInformation.keyDisplayAspectRatio); + + /// Returns average frame rate. + String? getAverageFrameRate() => + this.getStringProperty(StreamInformation.keyAverageFrameRate); + + /// Returns real frame rate. + String? getRealFrameRate() => + this.getStringProperty(StreamInformation.keyRealFrameRate); + + /// Returns time base. + String? getTimeBase() => + this.getStringProperty(StreamInformation.keyTimeBase); + + /// Returns codec time base. + String? getCodecTimeBase() => + this.getStringProperty(StreamInformation.keyCodecTimeBase); + + /// Returns all tags. + Map? getTags() => + this.getProperty(StreamInformation.keyTags); + + /// Returns the stream property associated with the key. + String? getStringProperty(String key) => this._allProperties?[key]; + + /// Returns the stream property associated with the key. + num? getNumberProperty(String key) => this._allProperties?[key]; + + /// Returns the stream property associated with the key. + dynamic getProperty(String key) => this._allProperties?[key]; + + /// Returns all properties found. + Map? getAllProperties() => this._allProperties; +} diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/pubspec.yaml b/packages/ffmpeg_kit_flutter_new_min_gpl/pubspec.yaml new file mode 100644 index 0000000..0d801c2 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/pubspec.yaml @@ -0,0 +1,26 @@ +name: ffmpeg_kit_flutter_new_min_gpl +description: FFmpeg Kit Min-GPL for Flutter. Supports Android, iOS, macOS and Windows platforms. +repository: https://github.com/sk3llo/ffmpeg_kit_flutter +homepage: https://github.com/sk3llo/ffmpeg_kit_flutter +version: 2.6.2 +environment: + sdk: ">=3.1.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + platforms: + android: + package: com.antonkarpenko.ffmpegkit + pluginClass: FFmpegKitFlutterPlugin + +dependencies: + ffmpeg_kit_flutter_platform_interface: ^0.2.1 + + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.1.8 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/scripts/build_spm_artifacts.sh b/packages/ffmpeg_kit_flutter_new_min_gpl/scripts/build_spm_artifacts.sh new file mode 100644 index 0000000..7fd5101 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/scripts/build_spm_artifacts.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# Build SPM-consumable combined XCFrameworks (iOS device + iOS simulator + macOS) +# from the ffmpeg-kit release zips of a variant, zip them for use as SPM binary +# targets, and emit their checksums. +# +# Usage: build_spm_artifacts.sh +# e.g. build_spm_artifacts.sh full-gpl 8.1.2 +# +# Inputs: GitHub release assets ffmpeg-kit-{ios,macos}--.zip +# under tag -. +# Outputs: out/.xcframework.zip (one XCFramework at the zip root, as SPM +# requires) and out/checksums.json, ready to upload to the same release. +# +# The iOS frameworks ship as fat Mach-O bundles (x86_64 sim + arm64/arm64e +# device). The device slice keeps arm64 only (arm64e is dropped, see #164). +# A native arm64 simulator slice is produced by retagging the device +# arm64 slice's LC_BUILD_VERSION platform from iOS (2) to iOS-Simulator (7) — +# FFmpeg is plain C with no platform-conditional linkage, so the retagged slice +# runs natively on Apple Silicon simulators (required by Xcode 26+). +set -euo pipefail + +VARIANT="${1:?usage: build_spm_artifacts.sh }" +VERSION="${2:?usage: build_spm_artifacts.sh }" +TAG="${VERSION}-${VARIANT}" +BASE="https://github.com/sk3llo/ffmpeg_kit_flutter/releases/download/${TAG}" +FRAMEWORKS="ffmpegkit libavcodec libavdevice libavfilter libavformat libavutil libswresample libswscale" + +WORK="spm-artifacts-${TAG}" +mkdir -p "$WORK" +cd "$WORK" + +[ -f ios.zip ] || curl -sSL "${BASE}/ffmpeg-kit-ios-${VARIANT}-${VERSION}.zip" -o ios.zip +[ -f macos.zip ] || curl -sSL "${BASE}/ffmpeg-kit-macos-${VARIANT}-${VERSION}.zip" -o macos.zip +rm -rf ios macos out stage +mkdir -p ios macos out stage +unzip -q -o ios.zip -d ios +unzip -q -o macos.zip -d macos +rm -rf ios/__MACOSX macos/__MACOSX + +# Strip bitcode everywhere (no-op if absent) +for FW in $FRAMEWORKS; do + for BIN in "ios/${FW}.framework/${FW}" "macos/${FW}.framework/${FW}"; do + [ -f "$BIN" ] && xcrun bitcode_strip -r "$BIN" -o "$BIN" || true + done +done + +convert() { + local FW="$1" + local IOS_DIR="ios/${FW}.framework" + local IOS_BIN="${IOS_DIR}/${FW}" + local MAC_DIR="macos/${FW}.framework" + local STAGE="stage/${FW}" + rm -rf "$STAGE" + mkdir -p "$STAGE/device" "$STAGE/sim" + + local ARCHS MINOS SDK + ARCHS=$(lipo -archs "$IOS_BIN") + MINOS=$(otool -l -arch arm64 "$IOS_BIN" 2>/dev/null | awk '/LC_BUILD_VERSION/{b=1} b&&/minos/{print $2; exit}') + SDK=$(otool -l -arch arm64 "$IOS_BIN" 2>/dev/null | awk '/LC_BUILD_VERSION/{b=1} b&&/sdk/{print $2; exit}') + [ -n "$MINOS" ] || MINOS="14.0" + [ -n "$SDK" ] || SDK="$MINOS" + + cp -R "$IOS_DIR" "$STAGE/device/${FW}.framework" + cp -R "$IOS_DIR" "$STAGE/sim/${FW}.framework" + + # Device slice: arm64 ONLY. arm64e is deliberately dropped: App Store apps + # run as arm64 (third-party arm64e needs a special entitlement), and App + # Store validation rejects arm64e slices built with pre-iOS-26 SDKs (#164). + lipo "$IOS_BIN" -thin arm64 -output "$STAGE/arm64.bin" + lipo -create "$STAGE/arm64.bin" -output "$STAGE/device/${FW}.framework/${FW}" + + # Simulator slice: x86_64 + arm64 retagged iOS(2) -> iOS-Simulator(7) + local SIM_ARGS=() + if echo "$ARCHS" | tr ' ' '\n' | grep -qx "x86_64"; then + lipo "$IOS_BIN" -thin x86_64 -output "$STAGE/x86_64.bin" + SIM_ARGS+=("$STAGE/x86_64.bin") + fi + if echo "$ARCHS" | tr ' ' '\n' | grep -qx "arm64"; then + lipo "$IOS_BIN" -thin arm64 -output "$STAGE/arm64-dev.bin" + vtool -arch arm64 -set-build-version 7 "$MINOS" "$SDK" -replace \ + -output "$STAGE/arm64-sim.bin" "$STAGE/arm64-dev.bin" + SIM_ARGS+=("$STAGE/arm64-sim.bin") + fi + lipo -create "${SIM_ARGS[@]}" -output "$STAGE/sim/${FW}.framework/${FW}" + + rm -rf "out/${FW}.xcframework" + xcodebuild -create-xcframework \ + -framework "$STAGE/device/${FW}.framework" \ + -framework "$STAGE/sim/${FW}.framework" \ + -framework "$MAC_DIR" \ + -output "out/${FW}.xcframework" >/dev/null + + # ditto preserves the macOS slice's Versions/ symlinks inside the zip + (cd out && ditto -c -k --keepParent "${FW}.xcframework" "${FW}.xcframework.zip") + rm -rf "$STAGE" +} + +for FW in $FRAMEWORKS; do + echo "== $FW" + convert "$FW" +done +rm -rf stage + +CK="out/checksums.json" +echo "{" > "$CK" +FIRST=1 +for FW in $FRAMEWORKS; do + SUM=$(swift package compute-checksum "out/${FW}.xcframework.zip") + [ $FIRST -eq 1 ] || echo "," >> "$CK" + FIRST=0 + printf ' "%s": "%s"' "$FW" "$SUM" >> "$CK" + echo "$FW $SUM" +done +echo "" >> "$CK" +echo "}" >> "$CK" + +echo +echo "Artifacts in ${WORK}/out — upload with:" +echo " gh release upload ${TAG} ${WORK}/out/*.xcframework.zip ${WORK}/out/checksums.json" diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/scripts/setup_ios.sh b/packages/ffmpeg_kit_flutter_new_min_gpl/scripts/setup_ios.sh new file mode 100644 index 0000000..8b200c7 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/scripts/setup_ios.sh @@ -0,0 +1,201 @@ +#!/bin/bash +# +# Downloads the prebuilt FFmpegKit iOS frameworks and installs them into +# ./Frameworks as .xcframeworks. Run from the pod root (the plugin's `ios/` +# directory) — the podspec's prepare_command does exactly that. +# +# Robustness (see issue #88 — "'ffmpegkit/FFmpegKitConfig.h' file not found"): +# * Atomic install: the frameworks appear in ./Frameworks only after a fully +# successful download + extraction + conversion + sanity check. A failure +# never leaves a half-populated ./Frameworks — which is what used to make the +# next `pod install` skip setup and produce a confusing "header not found" +# build error much later. +# * curl uses --fail/--retry so HTTP errors (404, proxy pages) and flaky +# networks fail loudly here instead of silently saving an error page as a zip. +# * The archive is verified before we trust it, and all 8 expected frameworks +# must be present afterwards. +# * Restricted networks (behind a firewall / in a blocked region) can point +# FFMPEG_KIT_IOS_URL at a reachable mirror of the release zip. +# +set -euo pipefail + +VERSION="8.1.2" +VARIANT="min-gpl" +DEFAULT_URL="https://github.com/sk3llo/ffmpeg_kit_flutter/releases/download/${VERSION}-${VARIANT}/ffmpeg-kit-ios-${VARIANT}-${VERSION}.zip" +IOS_URL="${FFMPEG_KIT_IOS_URL:-$DEFAULT_URL}" + +FRAMEWORKS="ffmpegkit libavcodec libavdevice libavfilter libavformat libavutil libswresample libswscale" + +# Self-healing / idempotent: if the frameworks are already installed, do nothing. +# The podspec guard checks the same marker, but re-checking here keeps the script +# safe to run manually and safe to re-run after a previous failure. +if [ -d "Frameworks/ffmpegkit.xcframework" ]; then + echo "[ffmpeg_kit_flutter] iOS frameworks already present — skipping download." + exit 0 +fi + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/ffmpegkit-ios.XXXXXX")" +cleanup() { rm -rf "$WORK"; } +trap cleanup EXIT + +fail() { + { + echo "" + echo "======================================================================" + echo "[ffmpeg_kit_flutter] ERROR: could not set up the iOS frameworks." + echo " $1" + echo "" + echo "The prebuilt FFmpegKit frameworks are downloaded at build time from:" + echo " $IOS_URL" + echo "" + echo "If your network cannot reach GitHub release assets (e.g. behind a" + echo "corporate firewall or in a restricted region), either:" + echo "" + echo " 1. Mirror the zip somewhere reachable and set FFMPEG_KIT_IOS_URL" + echo " before installing pods:" + echo " export FFMPEG_KIT_IOS_URL=\"https://your-mirror/ffmpeg-kit-ios-${VARIANT}-${VERSION}.zip\"" + echo " cd ios && pod install" + echo "" + echo " 2. Or download the zip manually and re-run this script from the" + echo " plugin's ios/ directory:" + echo " ../scripts/setup_ios.sh" + echo "======================================================================" + } >&2 + exit 1 +} + +# Build curl args, feature-detecting --retry-all-errors (curl >= 7.71). +CURL_ARGS=(-fL --retry 3 --retry-delay 2 --connect-timeout 30) +if curl --help all 2>/dev/null | grep -q -- '--retry-all-errors'; then + CURL_ARGS+=(--retry-all-errors) +fi + +echo "[ffmpeg_kit_flutter] Downloading iOS frameworks ($VARIANT $VERSION)..." +echo "[ffmpeg_kit_flutter] $IOS_URL" +if ! curl "${CURL_ARGS[@]}" -o "$WORK/frameworks.zip" "$IOS_URL"; then + fail "The download failed (see the curl output above)." +fi + +# Reject empty / truncated / non-zip payloads before trusting them. +[ -s "$WORK/frameworks.zip" ] || fail "The downloaded file is empty." +if ! unzip -tq "$WORK/frameworks.zip" >/dev/null 2>&1; then + fail "The downloaded file is not a valid zip (likely a partial download or an error page)." +fi + +mkdir -p "$WORK/extract" +unzip -oq "$WORK/frameworks.zip" -d "$WORK/extract" +rm -rf "$WORK/extract/__MACOSX" + +# Verify the archive actually contained the fat frameworks we expect. +for FW in $FRAMEWORKS; do + [ -d "$WORK/extract/${FW}.framework" ] || \ + fail "The archive is missing ${FW}.framework — it may be corrupt or the wrong build." +done + +# Delete bitcode from all frameworks. +for FW in $FRAMEWORKS; do + BIN="$WORK/extract/${FW}.framework/${FW}" + [ -f "$BIN" ] && xcrun bitcode_strip -r "$BIN" -o "$BIN" +done + +# +# Convert each fat .framework into an .xcframework that exposes a real +# arm64 iOS-simulator slice. +# +# The released frameworks are fat Mach-O bundles containing: +# x86_64 -> iOS simulator (platform 7) +# arm64 -> iOS device (platform 2) +# arm64e -> iOS device (platform 2) +# +# A fat framework cannot hold both a device-arm64 and a simulator-arm64 slice +# (same CPU type), which is why arm64 had to be excluded for the simulator. +# Apple Silicon simulators on Xcode 26 / iOS 26+ require a native arm64 +# simulator slice, so we split each framework into two .xcframework variants: +# ios-arm64 (device; arm64e is dropped, see #164) +# ios-arm64_x86_64-simulator (simulator: x86_64 + arm64 retagged via vtool) +# +# The arm64 simulator slice is produced by retagging the device arm64 slice's +# build-version platform from iOS (2) to iOS-Simulator (7). FFmpeg is plain C +# with no platform-conditional linkage, so the instructions are identical and +# the retagged slice runs natively on Apple Silicon simulators. +# +# BASE is the directory holding the fat .framework inputs; the resulting +# .xcframework is written alongside them and later moved into ./Frameworks. +# +convert_to_xcframework() { + local FW="$1" + local BASE="$2" + local DIR="${BASE}/${FW}.framework" + local BIN="${DIR}/${FW}" + [ -d "$DIR" ] || return 0 + + local ARCHS + ARCHS=$(lipo -archs "$BIN") + + # Read the device deployment target / sdk so vtool reproduces them. + local MINOS SDK + MINOS=$(otool -l -arch arm64 "$BIN" 2>/dev/null | awk '/LC_BUILD_VERSION/{b=1} b&&/minos/{print $2; exit}') + SDK=$(otool -l -arch arm64 "$BIN" 2>/dev/null | awk '/LC_BUILD_VERSION/{b=1} b&&/sdk/{print $2; exit}') + [ -n "$MINOS" ] || MINOS="14.0" + [ -n "$SDK" ] || SDK="$MINOS" + + local STAGE="${BASE}/.xc_tmp/${FW}" + rm -rf "$STAGE" + mkdir -p "$STAGE" + cp -R "$DIR" "$STAGE/device.framework" + cp -R "$DIR" "$STAGE/sim.framework" + # xcodebuild requires the inner framework names to match the binary name. + mv "$STAGE/device.framework" "$STAGE/device/${FW}.framework" 2>/dev/null || { mkdir -p "$STAGE/device"; mv "$STAGE/device.framework" "$STAGE/device/${FW}.framework"; } + mkdir -p "$STAGE/sim" + mv "$STAGE/sim.framework" "$STAGE/sim/${FW}.framework" + + # --- Device slice: arm64 ONLY. arm64e is deliberately dropped: App Store + # apps run as arm64 (third-party arm64e needs a special entitlement), and + # App Store validation rejects arm64e slices built with pre-iOS-26 SDKs (#164). + lipo "$BIN" -thin arm64 -output "$STAGE/arm64.dylib" + lipo -create "$STAGE/arm64.dylib" -output "$STAGE/device/${FW}.framework/${FW}" + + # --- Simulator slice: x86_64 (+ arm64 retagged to simulator) --- + local SIM_ARGS=() + if echo "$ARCHS" | tr ' ' '\n' | grep -qx "x86_64"; then + lipo "$BIN" -thin x86_64 -output "$STAGE/x86_64.dylib" + SIM_ARGS+=("$STAGE/x86_64.dylib") + fi + if echo "$ARCHS" | tr ' ' '\n' | grep -qx "arm64"; then + lipo "$BIN" -thin arm64 -output "$STAGE/arm64-dev.dylib" + vtool -arch arm64 -set-build-version 7 "$MINOS" "$SDK" -replace \ + -output "$STAGE/arm64-sim.dylib" "$STAGE/arm64-dev.dylib" + SIM_ARGS+=("$STAGE/arm64-sim.dylib") + fi + lipo -create "${SIM_ARGS[@]}" -output "$STAGE/sim/${FW}.framework/${FW}" + + # --- Assemble the xcframework --- + rm -rf "${BASE}/${FW}.xcframework" + xcodebuild -create-xcframework \ + -framework "$STAGE/device/${FW}.framework" \ + -framework "$STAGE/sim/${FW}.framework" \ + -output "${BASE}/${FW}.xcframework" + + rm -rf "$DIR" "$STAGE" +} + +echo "[ffmpeg_kit_flutter] Building xcframeworks..." +for FW in $FRAMEWORKS; do + convert_to_xcframework "$FW" "$WORK/extract" +done +rm -rf "$WORK/extract/.xc_tmp" + +# Verify every xcframework was produced before we touch ./Frameworks. +for FW in $FRAMEWORKS; do + [ -d "$WORK/extract/${FW}.xcframework" ] || \ + fail "Failed to build ${FW}.xcframework (Xcode command line tools required)." +done + +# --- Atomic install: only now do we populate ./Frameworks --- +rm -rf Frameworks +mkdir -p Frameworks +for FW in $FRAMEWORKS; do + mv "$WORK/extract/${FW}.xcframework" "Frameworks/${FW}.xcframework" +done + +echo "[ffmpeg_kit_flutter] iOS frameworks installed successfully." diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/scripts/setup_macos.sh b/packages/ffmpeg_kit_flutter_new_min_gpl/scripts/setup_macos.sh new file mode 100644 index 0000000..4f28311 --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/scripts/setup_macos.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# +# Downloads the prebuilt FFmpegKit macOS frameworks and installs them into +# ./Frameworks. Run from the pod root (the plugin's `macos/` directory) — the +# podspec's prepare_command does exactly that. +# +# Same robustness contract as setup_ios.sh (see issue #88): +# * Atomic install — ./Frameworks is only populated after a fully successful +# download + extraction + sanity check, so a failure never leaves a broken +# half-populated directory behind (which is what made the next `pod install` +# skip setup and produce a confusing "header not found" build error). +# * curl --fail/--retry surfaces network errors here instead of later. +# * FFMPEG_KIT_MACOS_URL overrides the download URL for restricted networks. +# +set -euo pipefail + +VERSION="8.1.2" +VARIANT="min-gpl" +DEFAULT_URL="https://github.com/sk3llo/ffmpeg_kit_flutter/releases/download/${VERSION}-${VARIANT}/ffmpeg-kit-macos-${VARIANT}-${VERSION}.zip" +MACOS_URL="${FFMPEG_KIT_MACOS_URL:-$DEFAULT_URL}" + +FRAMEWORKS="ffmpegkit libavcodec libavdevice libavfilter libavformat libavutil libswresample libswscale" + +# Self-healing / idempotent. +if [ -d "Frameworks/ffmpegkit.framework" ]; then + echo "[ffmpeg_kit_flutter] macOS frameworks already present — skipping download." + exit 0 +fi + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/ffmpegkit-macos.XXXXXX")" +cleanup() { rm -rf "$WORK"; } +trap cleanup EXIT + +fail() { + { + echo "" + echo "======================================================================" + echo "[ffmpeg_kit_flutter] ERROR: could not set up the macOS frameworks." + echo " $1" + echo "" + echo "The prebuilt FFmpegKit frameworks are downloaded at build time from:" + echo " $MACOS_URL" + echo "" + echo "If your network cannot reach GitHub release assets, either mirror the" + echo "zip and set FFMPEG_KIT_MACOS_URL before installing pods:" + echo " export FFMPEG_KIT_MACOS_URL=\"https://your-mirror/ffmpeg-kit-macos-${VARIANT}-${VERSION}.zip\"" + echo " cd macos && pod install" + echo "or download it manually and re-run ../scripts/setup_macos.sh." + echo "======================================================================" + } >&2 + exit 1 +} + +CURL_ARGS=(-fL --retry 3 --retry-delay 2 --connect-timeout 30) +if curl --help all 2>/dev/null | grep -q -- '--retry-all-errors'; then + CURL_ARGS+=(--retry-all-errors) +fi + +echo "[ffmpeg_kit_flutter] Downloading macOS frameworks ($VARIANT $VERSION)..." +echo "[ffmpeg_kit_flutter] $MACOS_URL" +if ! curl "${CURL_ARGS[@]}" -o "$WORK/frameworks.zip" "$MACOS_URL"; then + fail "The download failed (see the curl output above)." +fi + +[ -s "$WORK/frameworks.zip" ] || fail "The downloaded file is empty." +if ! unzip -tq "$WORK/frameworks.zip" >/dev/null 2>&1; then + fail "The downloaded file is not a valid zip (likely a partial download or an error page)." +fi + +mkdir -p "$WORK/extract" +unzip -oq "$WORK/frameworks.zip" -d "$WORK/extract" +rm -rf "$WORK/extract/__MACOSX" + +# Verify all expected frameworks are present. +for FW in $FRAMEWORKS; do + [ -d "$WORK/extract/${FW}.framework" ] || \ + fail "The archive is missing ${FW}.framework — it may be corrupt or the wrong build." +done + +# Delete bitcode from all frameworks (required for App Store submission). +for FW in $FRAMEWORKS; do + BIN="$WORK/extract/${FW}.framework/${FW}" + [ -f "$BIN" ] && xcrun bitcode_strip -r "$BIN" -o "$BIN" +done + +# --- Atomic install --- +rm -rf Frameworks +mkdir -p Frameworks +for FW in $FRAMEWORKS; do + mv "$WORK/extract/${FW}.framework" "Frameworks/${FW}.framework" +done + +echo "[ffmpeg_kit_flutter] macOS frameworks installed successfully." diff --git a/packages/flutter_inappwebview_android/CHANGELOG.md b/packages/flutter_inappwebview_android/CHANGELOG.md new file mode 100644 index 0000000..3170f86 --- /dev/null +++ b/packages/flutter_inappwebview_android/CHANGELOG.md @@ -0,0 +1,103 @@ +## 1.1.3 + +- Updated flutter_inappwebview_platform_interface version to ^1.3.0 + +## 1.1.2 + +- Removed webview/plugin_scripts_js/ConsoleLogJS.java file, use native WebChromeClient.onConsoleMessage instead + +## 1.1.1 + +- Updated flutter_inappwebview_platform_interface version to ^1.2.0 + +## 1.1.0+4 + +- Updated flutter_inappwebview_platform_interface version + +## 1.1.0+3 + +- Fixed compilation error + +## 1.1.0+2 + +- Updated pubspec.yaml + +## 1.1.0+1 + +- Downgraded androidx.appcompat:appcompat:1.7.0 to androidx.appcompat:appcompat:1.6.1 +- Added `-dontwarn android.window.BackEvent` proguard rule + +## 1.1.0 + +- Updated androidx.webkit:webkit:1.8.0 to androidx.webkit:webkit:1.12.0 +- Updated androidx.browser:browser:1.6.0 to androidx.browser:browser:1.8.0 +- Updates minimum supported SDK version to Flutter 3.24/Dart 3.5. +- Removed unsupported WebViewFeature.SUPPRESS_ERROR_PAGE +- Merged "Remove references to deprecated v1 Android embedding" [#2176](https://github.com/pichillilorenzo/flutter_inappwebview/pull/2176) (thanks to [gmackall](https://github.com/gmackall)) + +## 1.0.13 + +- Fixed "Android emulator using API 34 fails to draw on resume sometimes" [#1981](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1981) + +## 1.0.12 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.10` + +## 1.0.11 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.9` +- Fix typos (thanks to [michalsrutek](https://github.com/michalsrutek)) + +## 1.0.10 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.8` +- Implemented `PlatformCustomPathHandler` class + +## 1.0.9 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.7` +- Fixed "Cloudflare Turnstile failure" [#1738](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1738) +- Fixed `InAppWebViewController.callAsyncJavaScript` issue when the last line of the `functionBody` parameter includes a code comment + +## 1.0.8 + +- Implemented `InAppWebViewSettings.interceptOnlyAsyncAjaxRequests` +- Implemented `PlatformInAppWebViewController.clearFormData` method +- Implemented `PlatformCookieManager.removeSessionCookies` method +- Updated `useShouldInterceptAjaxRequest` automatic infer logic +- Updated `CookieManager` methods return value + +## 1.0.7 + +- Merged "Fixed error in InterceptAjaxRequestJS 'Failed to set responseType property'" [#1904](https://github.com/pichillilorenzo/flutter_inappwebview/pull/1904) (thanks to [EArminjon](https://github.com/EArminjon)) +- Fixed shouldInterceptAjaxRequest javascript code when overriding XMLHttpRequest.open method parameters + +## 1.0.6 + +- Fixed "getFavicons: _TypeError: type '_Map' is not a subtype of type 'Iterable'" [#1897](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1897) +- Fixed "onClosed not considering back navigation or up button / close button in ChromeSafariBrowser when using noHistory: true" [#1882](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1882) + +## 1.0.5 + +- Call `super.dispose();` on `InAppBrowser` and `ChromeSafari` implementations + +## 1.0.4 + +- Throw platform exception when ProcessGlobalConfig.apply throws an error on the native side to be able to catch it on Flutter side + +## 1.0.3 + +- Updated `ContentBlockerHandler` CSS_DISPLAY_NONE action type and `JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_SOURCE` javascript implementation code + +## 1.0.2 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `1.0.2` +- Fixed "Crash when starting ChromeSafariBrowser on Android java.lang.NoSuchMethodError: No virtual method isEngagementSignalsApiAvailable" [#1881](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1881) + +## 1.0.1 + +- Updated README + +## 1.0.0 + +Initial release. diff --git a/packages/flutter_inappwebview_android/JJ_PATCH.md b/packages/flutter_inappwebview_android/JJ_PATCH.md new file mode 100644 index 0000000..6504834 --- /dev/null +++ b/packages/flutter_inappwebview_android/JJ_PATCH.md @@ -0,0 +1,10 @@ +# flutter_inappwebview_android 1.1.3 사본 (JJ_MKVMaker 패치) + +원본: https://pub.dev/packages/flutter_inappwebview_android 1.1.3 (pubspec.yaml 의 dependency_overrides 로 사용) + +## 고친 문제 +새 Android 빌드 도구 (AGP 9) 가 `getDefaultProguardFile('proguard-android.txt')` 를 더는 받지 않아 APK 빌드가 실패함. + +## 수정 +- android/build.gradle: `proguard-android.txt` → `proguard-android-optimize.txt` +- example 폴더 제거 diff --git a/packages/flutter_inappwebview_android/LICENSE b/packages/flutter_inappwebview_android/LICENSE new file mode 100644 index 0000000..6ccd8da --- /dev/null +++ b/packages/flutter_inappwebview_android/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Lorenzo Pichilli + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/flutter_inappwebview_android/README.md b/packages/flutter_inappwebview_android/README.md new file mode 100644 index 0000000..77d0be6 --- /dev/null +++ b/packages/flutter_inappwebview_android/README.md @@ -0,0 +1,13 @@ +# flutter\_inappwebview\_android + +The Android WebView implementation of [`flutter_inappwebview`](https://pub.dev/packages/flutter_inappwebview). + +## Usage + +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), +which means you can simply use `flutter_inappwebview` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/analysis_options.yaml b/packages/flutter_inappwebview_android/analysis_options.yaml new file mode 100644 index 0000000..84964c3 --- /dev/null +++ b/packages/flutter_inappwebview_android/analysis_options.yaml @@ -0,0 +1,15 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + constant_identifier_names: ignore + deprecated_member_use_from_same_package: ignore + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options +analyzer: + errors: + deprecated_member_use: ignore + deprecated_member_use_from_same_package: ignore + unnecessary_cast: ignore + unnecessary_import: ignore diff --git a/packages/flutter_inappwebview_android/android/build.gradle b/packages/flutter_inappwebview_android/android/build.gradle new file mode 100644 index 0000000..686e878 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/build.gradle @@ -0,0 +1,57 @@ +group 'com.pichillilorenzo.flutter_inappwebview_android' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.0.1' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'com.pichillilorenzo.flutter_inappwebview_android' + } + compileSdk 34 + + defaultConfig { + minSdkVersion 19 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + vectorDrawables.useSupportLibrary = true + consumerProguardFiles 'proguard-rules.pro' + } + lintOptions { + disable 'InvalidPackage' + } + buildTypes { + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + dependencies { + implementation 'androidx.webkit:webkit:1.12.0' + implementation 'androidx.browser:browser:1.8.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + } +} diff --git a/packages/flutter_inappwebview_android/android/gradle.properties b/packages/flutter_inappwebview_android/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/packages/flutter_inappwebview_android/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/flutter_inappwebview_android/android/proguard-rules.pro b/packages/flutter_inappwebview_android/android/proguard-rules.pro new file mode 100644 index 0000000..a806bdc --- /dev/null +++ b/packages/flutter_inappwebview_android/android/proguard-rules.pro @@ -0,0 +1,19 @@ +# WebView +-keepattributes *JavascriptInterface* +-keepclassmembers class * { + @android.webkit.JavascriptInterface ; +} +-keepclassmembers class * extends android.webkit.WebViewClient { + public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap); + public boolean *(android.webkit.WebView, java.lang.String); + public void *(android.webkit.webView, jav.lang.String); +} +-keepclassmembers class com.pichillilorenzo.flutter_inappwebview_android$JavaScriptBridgeInterface { + ; + ; + public *; + private *; +} +-keep class com.pichillilorenzo.flutter_inappwebview_android.** { *; } + +-dontwarn android.window.BackEvent \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/settings.gradle b/packages/flutter_inappwebview_android/android/settings.gradle new file mode 100644 index 0000000..5841fff --- /dev/null +++ b/packages/flutter_inappwebview_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_inappwebview_android' diff --git a/packages/flutter_inappwebview_android/android/src/main/AndroidManifest.xml b/packages/flutter_inappwebview_android/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c8036b2 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/ISettings.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/ISettings.java new file mode 100644 index 0000000..c005c2e --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/ISettings.java @@ -0,0 +1,11 @@ +package com.pichillilorenzo.flutter_inappwebview_android; + +import androidx.annotation.NonNull; + +import java.util.Map; + +public interface ISettings { + @NonNull ISettings parse(@NonNull Map settings); + @NonNull Map toMap(); + @NonNull Map getRealSettings(@NonNull T obj); +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFileProvider.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFileProvider.java new file mode 100644 index 0000000..3db8636 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFileProvider.java @@ -0,0 +1,9 @@ +package com.pichillilorenzo.flutter_inappwebview_android; + +import androidx.core.content.FileProvider; + +public class InAppWebViewFileProvider extends FileProvider { + + public static final String fileProviderAuthorityExtension = "flutter_inappwebview_android.fileprovider"; + +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFlutterPlugin.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFlutterPlugin.java new file mode 100644 index 0000000..21ce254 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFlutterPlugin.java @@ -0,0 +1,224 @@ +package com.pichillilorenzo.flutter_inappwebview_android; + +import android.app.Activity; +import android.content.Context; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ChromeSafariBrowserManager; +import com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.NoHistoryCustomTabsActivityCallbacks; +import com.pichillilorenzo.flutter_inappwebview_android.credential_database.CredentialDatabaseHandler; +import com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview.HeadlessInAppWebViewManager; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserManager; +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobManager; +import com.pichillilorenzo.flutter_inappwebview_android.process_global_config.ProcessGlobalConfigManager; +import com.pichillilorenzo.flutter_inappwebview_android.proxy.ProxyManager; +import com.pichillilorenzo.flutter_inappwebview_android.service_worker.ServiceWorkerManager; +import com.pichillilorenzo.flutter_inappwebview_android.tracing.TracingControllerManager; +import com.pichillilorenzo.flutter_inappwebview_android.webview.FlutterWebViewFactory; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewManager; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.platform.PlatformViewRegistry; +import io.flutter.embedding.android.FlutterView; + +public class InAppWebViewFlutterPlugin implements FlutterPlugin, ActivityAware { + + protected static final String LOG_TAG = "InAppWebViewFlutterPL"; + + @Nullable + public PlatformUtil platformUtil; + @Nullable + public InAppBrowserManager inAppBrowserManager; + @Nullable + public HeadlessInAppWebViewManager headlessInAppWebViewManager; + @Nullable + public ChromeSafariBrowserManager chromeSafariBrowserManager; + @Nullable + public NoHistoryCustomTabsActivityCallbacks noHistoryCustomTabsActivityCallbacks; + @Nullable + public InAppWebViewManager inAppWebViewManager; + @Nullable + public MyCookieManager myCookieManager; + @Nullable + public CredentialDatabaseHandler credentialDatabaseHandler; + @Nullable + public MyWebStorage myWebStorage; + @Nullable + public ServiceWorkerManager serviceWorkerManager; + @Nullable + public WebViewFeatureManager webViewFeatureManager; + @Nullable + public ProxyManager proxyManager; + @Nullable + public PrintJobManager printJobManager; + @Nullable + public TracingControllerManager tracingControllerManager; + @Nullable + public ProcessGlobalConfigManager processGlobalConfigManager; + public FlutterWebViewFactory flutterWebViewFactory; + public Context applicationContext; + public BinaryMessenger messenger; + public FlutterPlugin.FlutterAssets flutterAssets; + @Nullable + public ActivityPluginBinding activityPluginBinding; + @Nullable + public Activity activity; + public FlutterView flutterView; + + public InAppWebViewFlutterPlugin() {} + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + this.flutterAssets = binding.getFlutterAssets(); + + // Shared.activity could be null or not. + // It depends on who is called first between onAttachedToEngine event and onAttachedToActivity event. + // + // See https://github.com/pichillilorenzo/flutter_inappwebview/issues/390#issuecomment-647039084 + onAttachedToEngine( + binding.getApplicationContext(), binding.getBinaryMessenger(), this.activity, binding.getPlatformViewRegistry(), null); + } + + private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger, Activity activity, PlatformViewRegistry platformViewRegistry, FlutterView flutterView) { + this.applicationContext = applicationContext; + this.activity = activity; + this.messenger = messenger; + this.flutterView = flutterView; + + inAppBrowserManager = new InAppBrowserManager(this); + headlessInAppWebViewManager = new HeadlessInAppWebViewManager(this); + chromeSafariBrowserManager = new ChromeSafariBrowserManager(this); + noHistoryCustomTabsActivityCallbacks = new NoHistoryCustomTabsActivityCallbacks(this); + flutterWebViewFactory = new FlutterWebViewFactory(this); + platformViewRegistry.registerViewFactory( + FlutterWebViewFactory.VIEW_TYPE_ID, flutterWebViewFactory); + + platformUtil = new PlatformUtil(this); + inAppWebViewManager = new InAppWebViewManager(this); + myCookieManager = new MyCookieManager(this); + myWebStorage = new MyWebStorage(this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + serviceWorkerManager = new ServiceWorkerManager(this); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + credentialDatabaseHandler = new CredentialDatabaseHandler(this); + } + webViewFeatureManager = new WebViewFeatureManager(this); + proxyManager = new ProxyManager(this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + printJobManager = new PrintJobManager(this); + } + tracingControllerManager = new TracingControllerManager(this); + processGlobalConfigManager = new ProcessGlobalConfigManager(this); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + if (platformUtil != null) { + platformUtil.dispose(); + platformUtil = null; + } + if (inAppBrowserManager != null) { + inAppBrowserManager.dispose(); + inAppBrowserManager = null; + } + if (headlessInAppWebViewManager != null) { + headlessInAppWebViewManager.dispose(); + headlessInAppWebViewManager = null; + } + if (chromeSafariBrowserManager != null) { + chromeSafariBrowserManager.dispose(); + chromeSafariBrowserManager = null; + } + if (noHistoryCustomTabsActivityCallbacks != null) { + noHistoryCustomTabsActivityCallbacks.dispose(); + noHistoryCustomTabsActivityCallbacks = null; + } + if (myCookieManager != null) { + myCookieManager.dispose(); + myCookieManager = null; + } + if (myWebStorage != null) { + myWebStorage.dispose(); + myWebStorage = null; + } + if (credentialDatabaseHandler != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + credentialDatabaseHandler.dispose(); + credentialDatabaseHandler = null; + } + if (inAppWebViewManager != null) { + inAppWebViewManager.dispose(); + inAppWebViewManager = null; + } + if (serviceWorkerManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + serviceWorkerManager.dispose(); + serviceWorkerManager = null; + } + if (webViewFeatureManager != null) { + webViewFeatureManager.dispose(); + webViewFeatureManager = null; + } + if (proxyManager != null) { + proxyManager.dispose(); + proxyManager = null; + } + if (printJobManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + printJobManager.dispose(); + printJobManager = null; + } + if (tracingControllerManager != null) { + tracingControllerManager.dispose(); + tracingControllerManager = null; + } + if (processGlobalConfigManager != null) { + processGlobalConfigManager.dispose(); + processGlobalConfigManager = null; + } + } + + @Override + public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) { + this.activityPluginBinding = activityPluginBinding; + this.activity = activityPluginBinding.getActivity(); + + if (noHistoryCustomTabsActivityCallbacks != null) { + this.activity.getApplication().registerActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks); + } + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + if (activity != null && noHistoryCustomTabsActivityCallbacks != null) { + this.activity.getApplication().unregisterActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks); + } + + activityPluginBinding = null; + activity = null; + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) { + this.activityPluginBinding = activityPluginBinding; + this.activity = activityPluginBinding.getActivity(); + + if (noHistoryCustomTabsActivityCallbacks != null) { + this.activity.getApplication().registerActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks); + } + } + + @Override + public void onDetachedFromActivity() { + if (activity != null && noHistoryCustomTabsActivityCallbacks != null) { + this.activity.getApplication().unregisterActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks); + } + + activityPluginBinding = null; + activity = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyCookieManager.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyCookieManager.java new file mode 100644 index 0000000..ff4247e --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyCookieManager.java @@ -0,0 +1,437 @@ +package com.pichillilorenzo.flutter_inappwebview_android; + +import android.os.Build; +import android.util.Log; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.ValueCallback; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.CookieManagerCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class MyCookieManager extends ChannelDelegateImpl { + protected static final String LOG_TAG = "MyCookieManager"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_cookiemanager"; + @Nullable + public static CookieManager cookieManager; + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public MyCookieManager(@NonNull final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.plugin = plugin; + } + + public static void init() { + if (cookieManager == null) { + cookieManager = getCookieManager(); + } + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + init(); + + switch (call.method) { + case "setCookie": + { + String url = (String) call.argument("url"); + String name = (String) call.argument("name"); + String value = (String) call.argument("value"); + String domain = (String) call.argument("domain"); + String path = (String) call.argument("path"); + String expiresDateString = (String) call.argument("expiresDate"); + Long expiresDate = (expiresDateString != null ? new Long(expiresDateString) : null); + Integer maxAge = (Integer) call.argument("maxAge"); + Boolean isSecure = (Boolean) call.argument("isSecure"); + Boolean isHttpOnly = (Boolean) call.argument("isHttpOnly"); + String sameSite = (String) call.argument("sameSite"); + setCookie(url, + name, + value, + domain, + path, + expiresDate, + maxAge, + isSecure, + isHttpOnly, + sameSite, + result); + } + break; + case "getCookies": + result.success(getCookies((String) call.argument("url"))); + break; + case "deleteCookie": + { + String url = (String) call.argument("url"); + String name = (String) call.argument("name"); + String domain = (String) call.argument("domain"); + String path = (String) call.argument("path"); + deleteCookie(url, name, domain, path, result); + } + break; + case "deleteCookies": + { + String url = (String) call.argument("url"); + String domain = (String) call.argument("domain"); + String path = (String) call.argument("path"); + deleteCookies(url, domain, path, result); + } + break; + case "deleteAllCookies": + deleteAllCookies(result); + break; + case "removeSessionCookies": + removeSessionCookies(result); + break; + default: + result.notImplemented(); + } + } + + /** + * Instantiating CookieManager will load the Chromium task taking a 100ish ms so we do it lazily + * to make sure it's done on a background thread as needed. + * + * https://github.com/facebook/react-native/blob/1903f6680d9750e244d97c3cd4a9f755a9a47c61/ReactAndroid/src/main/java/com/facebook/react/modules/network/ForwardingCookieHandler.java#L132 + */ + static private @Nullable CookieManager getCookieManager() { + if (cookieManager == null) { + try { + cookieManager = CookieManager.getInstance(); + } catch (IllegalArgumentException ex) { + // https://bugs.chromium.org/p/chromium/issues/detail?id=559720 + return null; + } catch (Exception exception) { + String message = exception.getMessage(); + // We cannot catch MissingWebViewPackageException as it is in a private / system API + // class. This validates the exception's message to ensure we are only handling this + // specific exception. + // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/webkit/WebViewFactory.java#348 + if (message != null + && exception + .getClass() + .getCanonicalName() + .equals("android.webkit.WebViewFactory.MissingWebViewPackageException")) { + return null; + } else { + throw exception; + } + } + } + + return cookieManager; + } + + public void setCookie(String url, + String name, + String value, + String domain, + String path, + Long expiresDate, + Integer maxAge, + Boolean isSecure, + Boolean isHttpOnly, + String sameSite, + final MethodChannel.Result result) { + cookieManager = getCookieManager(); + if (cookieManager == null) { + result.success(false); + return; + } + + String cookieValue = name + "=" + value + "; Path=" + path; + + if (domain != null) + cookieValue += "; Domain=" + domain; + + if (expiresDate != null) + cookieValue += "; Expires=" + getCookieExpirationDate(expiresDate); + + if (maxAge != null) + cookieValue += "; Max-Age=" + maxAge.toString(); + + if (isSecure != null && isSecure) + cookieValue += "; Secure"; + + if (isHttpOnly != null && isHttpOnly) + cookieValue += "; HttpOnly"; + + if (sameSite != null) + cookieValue += "; SameSite=" + sameSite; + + cookieValue += ";"; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.setCookie(url, cookieValue, new ValueCallback() { + @Override + public void onReceiveValue(Boolean successful) { + result.success(successful); + } + }); + cookieManager.flush(); + } + else if (plugin != null) { + CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext); + cookieSyncMngr.startSync(); + cookieManager.setCookie(url, cookieValue); + cookieSyncMngr.stopSync(); + cookieSyncMngr.sync(); + result.success(true); + } else { + cookieManager.setCookie(url, cookieValue); + result.success(true); + } + } + + public List> getCookies(final String url) { + + final List> cookieListMap = new ArrayList<>(); + + cookieManager = getCookieManager(); + if (cookieManager == null) return cookieListMap; + + List cookies = new ArrayList<>(); + if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_COOKIE_INFO)) { + cookies = CookieManagerCompat.getCookieInfo(cookieManager, url); + } else { + String cookiesString = cookieManager.getCookie(url); + if (cookiesString != null) { + cookies = Arrays.asList(cookiesString.split(";")); + } + } + + for (String cookie : cookies) { + String[] cookieParams = cookie.split(";"); + if (cookieParams.length == 0) continue; + + String[] nameValue = cookieParams[0].split("=", 2); + String name = nameValue[0].trim(); + String value = (nameValue.length > 1) ? nameValue[1].trim() : ""; + + Map cookieMap = new HashMap<>(); + cookieMap.put("name", name); + cookieMap.put("value", value); + cookieMap.put("expiresDate", null); + cookieMap.put("isSessionOnly", null); + cookieMap.put("domain", null); + cookieMap.put("sameSite", null); + cookieMap.put("isSecure", null); + cookieMap.put("isHttpOnly", null); + cookieMap.put("path", null); + + if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_COOKIE_INFO)) { + cookieMap.put("isSecure", false); + cookieMap.put("isHttpOnly", false); + + for (int i = 1; i < cookieParams.length; i++) { + String[] cookieParamNameValue = cookieParams[i].split("=", 2); + String cookieParamName = cookieParamNameValue[0].trim(); + String cookieParamValue = (cookieParamNameValue.length > 1) ? cookieParamNameValue[1].trim() : ""; + + if (cookieParamName.equalsIgnoreCase("Expires")) { + try { + final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z", Locale.US); + Date expiryDate = sdf.parse(cookieParamValue); + if (expiryDate != null) { + cookieMap.put("expiresDate", expiryDate.getTime()); + } + } catch (ParseException e) { + Log.e(LOG_TAG, "", e); + } + } else if (cookieParamName.equalsIgnoreCase("Max-Age")) { + try { + long maxAge = Long.parseLong(cookieParamValue); + cookieMap.put("expiresDate", System.currentTimeMillis() + maxAge); + } catch (NumberFormatException e) { + Log.e(LOG_TAG, "", e); + } + } else if (cookieParamName.equalsIgnoreCase("Domain")) { + cookieMap.put("domain", cookieParamValue); + } else if (cookieParamName.equalsIgnoreCase("SameSite")) { + cookieMap.put("sameSite", cookieParamValue); + } else if (cookieParamName.equalsIgnoreCase("Secure")) { + cookieMap.put("isSecure", true); + } else if (cookieParamName.equalsIgnoreCase("HttpOnly")) { + cookieMap.put("isHttpOnly", true); + } else if (cookieParamName.equalsIgnoreCase("Path")) { + cookieMap.put("path", cookieParamValue); + } + } + } + + cookieListMap.add(cookieMap); + } + return cookieListMap; + + } + + public void deleteCookie(String url, String name, String domain, String path, final MethodChannel.Result result) { + cookieManager = getCookieManager(); + if (cookieManager == null) { + result.success(false); + return; + } + + String cookieValue = name + "=; Path=" + path + "; Max-Age=-1"; + + if (domain != null) + cookieValue += "; Domain=" + domain; + + cookieValue += ";"; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.setCookie(url, cookieValue, new ValueCallback() { + @Override + public void onReceiveValue(Boolean successful) { + result.success(successful); + } + }); + cookieManager.flush(); + } + else if (plugin != null) { + CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext); + cookieSyncMngr.startSync(); + cookieManager.setCookie(url, cookieValue); + cookieSyncMngr.stopSync(); + cookieSyncMngr.sync(); + result.success(true); + } else { + cookieManager.setCookie(url, cookieValue); + result.success(true); + } + } + + public void deleteCookies(String url, String domain, String path, final MethodChannel.Result result) { + cookieManager = getCookieManager(); + if (cookieManager == null) { + result.success(false); + return; + } + + CookieSyncManager cookieSyncMngr = null; + + String cookiesString = cookieManager.getCookie(url); + if (cookiesString != null) { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && plugin != null) { + cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext); + cookieSyncMngr.startSync(); + } + + String[] cookies = cookiesString.split(";"); + for (String cookie : cookies) { + String[] nameValue = cookie.split("=", 2); + String name = nameValue[0].trim(); + + String cookieValue = name + "=; Path=" + path + "; Max-Age=-1"; + + if (domain != null) + cookieValue += "; Domain=" + domain; + + cookieValue += ";"; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + cookieManager.setCookie(url, cookieValue, null); + else + cookieManager.setCookie(url, cookieValue); + } + + if (cookieSyncMngr != null) { + cookieSyncMngr.stopSync(); + cookieSyncMngr.sync(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + cookieManager.flush(); + } + result.success(true); + } + + public void deleteAllCookies(final MethodChannel.Result result) { + cookieManager = getCookieManager(); + if (cookieManager == null) { + result.success(false); + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.removeAllCookies(new ValueCallback() { + @Override + public void onReceiveValue(Boolean successful) { + result.success(successful); + } + }); + cookieManager.flush(); + } + else if (plugin != null) { + CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext); + cookieSyncMngr.startSync(); + cookieManager.removeAllCookie(); + cookieSyncMngr.stopSync(); + cookieSyncMngr.sync(); + result.success(true); + } else { + cookieManager.removeAllCookie(); + result.success(true); + } + } + + public void removeSessionCookies(final MethodChannel.Result result) { + cookieManager = getCookieManager(); + if (cookieManager == null) { + result.success(false); + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.removeSessionCookies(new ValueCallback() { + @Override + public void onReceiveValue(Boolean successful) { + result.success(successful); + } + }); + cookieManager.flush(); + } + else if (plugin != null) { + CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext); + cookieSyncMngr.startSync(); + cookieManager.removeSessionCookie(); + cookieSyncMngr.stopSync(); + cookieSyncMngr.sync(); + result.success(true); + } else { + cookieManager.removeSessionCookie(); + result.success(true); + } + } + + public static String getCookieExpirationDate(Long timestamp) { + final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z", Locale.US); + sdf.setTimeZone(TimeZone.getTimeZone("GMT")); + return sdf.format(new Date(timestamp)); + } + + @Override + public void dispose() { + super.dispose(); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyWebStorage.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyWebStorage.java new file mode 100644 index 0000000..d826b58 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyWebStorage.java @@ -0,0 +1,138 @@ +package com.pichillilorenzo.flutter_inappwebview_android; + +import android.webkit.ValueCallback; +import android.webkit.WebStorage; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class MyWebStorage extends ChannelDelegateImpl { + protected static final String LOG_TAG = "MyWebStorage"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_webstoragemanager"; + + @Nullable + public static WebStorage webStorageManager; + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public MyWebStorage(@NonNull final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.plugin = plugin; + } + + public static void init() { + if (webStorageManager == null) { + webStorageManager = WebStorage.getInstance(); + } + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + init(); + + switch (call.method) { + case "getOrigins": + getOrigins(result); + break; + case "deleteAllData": + if (webStorageManager != null) { + webStorageManager.deleteAllData(); + result.success(true); + } else { + result.success(false); + } + break; + case "deleteOrigin": + { + if (webStorageManager != null) { + String origin = (String) call.argument("origin"); + webStorageManager.deleteOrigin(origin); + result.success(true); + } else { + result.success(false); + } + } + break; + case "getQuotaForOrigin": + { + String origin = (String) call.argument("origin"); + getQuotaForOrigin(origin, result); + } + break; + case "getUsageForOrigin": + { + String origin = (String) call.argument("origin"); + getUsageForOrigin(origin, result); + } + break; + default: + result.notImplemented(); + } + } + + public void getOrigins(final MethodChannel.Result result) { + if (webStorageManager == null) { + result.success(new ArrayList<>()); + return; + } + webStorageManager.getOrigins(new ValueCallback() { + @Override + public void onReceiveValue(Map value) { + List> origins = new ArrayList<>(); + for(Object key : value.keySet()) { + WebStorage.Origin originObj = (WebStorage.Origin) value.get(key); + + Map originInfo = new HashMap<>(); + originInfo.put("origin", originObj.getOrigin()); + originInfo.put("quota", originObj.getQuota()); + originInfo.put("usage", originObj.getUsage()); + + origins.add(originInfo); + } + result.success(origins); + } + }); + } + + public void getQuotaForOrigin(String origin, final MethodChannel.Result result) { + if (webStorageManager == null) { + result.success(0); + return; + } + webStorageManager.getQuotaForOrigin(origin, new ValueCallback() { + @Override + public void onReceiveValue(Long value) { + result.success(value); + } + }); + } + + public void getUsageForOrigin(String origin, final MethodChannel.Result result) { + if (webStorageManager == null) { + result.success(0); + return; + } + webStorageManager.getUsageForOrigin(origin, new ValueCallback() { + @Override + public void onReceiveValue(Long value) { + result.success(value); + } + }); + } + + @Override + public void dispose() { + super.dispose(); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/PlatformUtil.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/PlatformUtil.java new file mode 100644 index 0000000..7a2459f --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/PlatformUtil.java @@ -0,0 +1,73 @@ +package com.pichillilorenzo.flutter_inappwebview_android; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class PlatformUtil extends ChannelDelegateImpl { + protected static final String LOG_TAG = "PlatformUtil"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_platformutil"; + + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public PlatformUtil(final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.plugin = plugin; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) { + switch (call.method) { + case "getSystemVersion": + result.success(String.valueOf(Build.VERSION.SDK_INT)); + break; + case "formatDate": + long date = (long) call.argument("date"); + String format = (String) call.argument("format"); + Locale locale = PlatformUtil.getLocaleFromString((String) call.argument("locale")); + String timezone = (String) call.argument("timezone"); + if (timezone == null) { + timezone = "UTC"; + } + result.success(PlatformUtil.formatDate(date, format, locale, TimeZone.getTimeZone(timezone))); + break; + default: + result.notImplemented(); + } + } + + public static Locale getLocaleFromString(@Nullable String locale) { + if (locale == null) { + return Locale.US; + } + String[] localeSplit = locale.split("_"); + String language = localeSplit[0]; + String country = localeSplit.length > 1 ? localeSplit[1] : ""; + String variant = localeSplit.length > 2 ? localeSplit[2] : ""; + return new Locale(language, country, variant); + } + + public static String formatDate(long date, String format, Locale locale, TimeZone timezone) { + final SimpleDateFormat sdf = new SimpleDateFormat(format, locale); + sdf.setTimeZone(timezone); + return sdf.format(new Date(date)); + } + + @Override + public void dispose() { + super.dispose(); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/Util.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/Util.java new file mode 100644 index 0000000..1531562 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/Util.java @@ -0,0 +1,386 @@ +package com.pichillilorenzo.flutter_inappwebview_android; + +import android.content.Context; +import android.content.res.AssetManager; +import android.graphics.BitmapFactory; +import android.graphics.Insets; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.http.SslCertificate; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import com.pichillilorenzo.flutter_inappwebview_android.types.Size2D; +import com.pichillilorenzo.flutter_inappwebview_android.types.SyncBaseCallbackResultImpl; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.URL; +import java.net.UnknownHostException; +import java.security.Key; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; + +import javax.net.ssl.SSLHandshakeException; + +import io.flutter.plugin.common.MethodChannel; + +public class Util { + + static final String LOG_TAG = "Util"; + public static final String ANDROID_ASSET_URL = "file:///android_asset/"; + + private Util() {} + + public static String getUrlAsset(InAppWebViewFlutterPlugin plugin, String assetFilePath) throws IOException { + String key = plugin.flutterAssets.getAssetFilePathByName(assetFilePath); + InputStream is = null; + IOException e = null; + + try { + is = getFileAsset(plugin, assetFilePath); + } catch (IOException ex) { + e = ex; + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ex) { + e = ex; + } + } + } + if (e != null) { + throw e; + } + + return ANDROID_ASSET_URL + key; + } + + public static InputStream getFileAsset(InAppWebViewFlutterPlugin plugin, String assetFilePath) throws IOException { + String key = plugin.flutterAssets.getAssetFilePathByName(assetFilePath); + AssetManager mg = plugin.applicationContext.getResources().getAssets(); + return mg.open(key); + } + + public static T invokeMethodAndWaitResult(final @NonNull MethodChannel channel, + final @NonNull String method, final @Nullable Object arguments, + final @NonNull SyncBaseCallbackResultImpl callback) throws InterruptedException { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + @Override + public void run() { + channel.invokeMethod(method, arguments, callback); + } + }); + callback.latch.await(); + return callback.result; + } + + @Nullable + public static PrivateKeyAndCertificates loadPrivateKeyAndCertificate(@NonNull InAppWebViewFlutterPlugin plugin, + @NonNull String certificatePath, + @Nullable String certificatePassword, + @NonNull String keyStoreType) { + PrivateKeyAndCertificates privateKeyAndCertificates = null; + InputStream certificateFileStream = null; + + try { + certificateFileStream = getFileAsset(plugin, certificatePath); + } catch (IOException ignored) {} + + try { + if (certificateFileStream == null) { + certificateFileStream = new FileInputStream(certificatePath); + } + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(certificateFileStream, (certificatePassword != null ? certificatePassword : "").toCharArray()); + + Enumeration aliases = keyStore.aliases(); + String alias = aliases.nextElement(); + + Key key = keyStore.getKey(alias, (certificatePassword != null ? certificatePassword : "").toCharArray()); + if (key instanceof PrivateKey) { + PrivateKey privateKey = (PrivateKey)key; + Certificate cert = keyStore.getCertificate(alias); + X509Certificate[] certificates = new X509Certificate[1]; + certificates[0] = (X509Certificate)cert; + privateKeyAndCertificates = new PrivateKeyAndCertificates(privateKey, certificates); + } + certificateFileStream.close(); + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } finally { + if (certificateFileStream != null) { + try { + certificateFileStream.close(); + } catch (IOException ex) { + Log.e(LOG_TAG, "", ex); + } + } + } + + return privateKeyAndCertificates; + } + + public static class PrivateKeyAndCertificates { + + public X509Certificate[] certificates; + public PrivateKey privateKey; + + public PrivateKeyAndCertificates(PrivateKey privateKey, X509Certificate[] certificates) { + this.privateKey = privateKey; + this.certificates = certificates; + } + } + + @Nullable + public static HttpURLConnection makeHttpRequest(String urlString, String method, @Nullable Map headers) { + HttpURLConnection urlConnection = null; + try { + URL url = new URL(urlString); + urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setRequestMethod(method); + if (headers != null) { + for (Map.Entry header : headers.entrySet()) { + urlConnection.setRequestProperty(header.getKey(), header.getValue()); + } + } + urlConnection.setConnectTimeout(15000); // 15 seconds + urlConnection.setReadTimeout(15000); // 15 seconds + urlConnection.setDoInput(true); + urlConnection.setInstanceFollowRedirects(true); + if ("GET".equalsIgnoreCase(method)) { + urlConnection.setDoOutput(false); + } + urlConnection.connect(); + return urlConnection; + } + catch (Exception e) { + if (!(e instanceof SSLHandshakeException)) { + Log.e(LOG_TAG, "", e); + } + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + return null; + } + + /** + * SslCertificate class does not has a public getter for the underlying + * X509Certificate, we can only do this by hack. This only works for Android 4.0+ + * https://groups.google.com/forum/#!topic/android-developers/eAPJ6b7mrmg + */ + public static X509Certificate getX509CertFromSslCertHack(SslCertificate sslCert) { + X509Certificate x509Certificate = null; + + Bundle bundle = SslCertificate.saveState(sslCert); + byte[] bytes = bundle.getByteArray("x509-certificate"); + + if (bytes == null) { + x509Certificate = null; + } else { + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes)); + x509Certificate = (X509Certificate) cert; + } catch (CertificateException e) { + x509Certificate = null; + } + } + + return x509Certificate; + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public static String JSONStringify(@Nullable Object value) { + if (value == null) { + return "null"; + } + if (value instanceof Map) { + return new JSONObject((Map) value).toString(); + } else if (value instanceof List) { + return new JSONArray((List) value).toString(); + } else if (value instanceof String) { + return JSONObject.quote((String) value); + } else { + return JSONObject.wrap(value).toString(); + } + } + + public static boolean objEquals(@Nullable Object a, @Nullable Object b) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return Objects.equals(a, b); + } + return (a == b) || (a != null && a.equals(b)); + } + + public static String replaceAll(String s, String oldString, String newString) { + return TextUtils.join(newString, s.split(Pattern.quote(oldString))); + } + + public static void log(String tag, String message) { + // Split by line, then ensure each line can fit into Log's maximum length. + for (int i = 0, length = message.length(); i < length; i++) { + int newline = message.indexOf('\n', i); + newline = newline != -1 ? newline : length; + do { + int end = Math.min(newline, i + 4000); + Log.d(tag, message.substring(i, end)); + i = end; + } while (i < newline); + } + } + + public static float getPixelDensity(Context context) { + return context.getResources().getDisplayMetrics().density; + } + + public static Size2D getFullscreenSize(Context context) { + Size2D fullscreenSize = new Size2D(-1, -1); + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + if (wm != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + final WindowMetrics metrics = wm.getCurrentWindowMetrics(); + // Gets all excluding insets + final WindowInsets windowInsets = metrics.getWindowInsets(); + Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() + | WindowInsets.Type.displayCutout()); + int insetsWidth = insets.right + insets.left; + int insetsHeight = insets.top + insets.bottom; + final Rect bounds = metrics.getBounds(); + fullscreenSize.setWidth(bounds.width() - insetsWidth); + fullscreenSize.setHeight(bounds.height() - insetsHeight); + } else { + DisplayMetrics displayMetrics = new DisplayMetrics(); + wm.getDefaultDisplay().getMetrics(displayMetrics); + fullscreenSize.setWidth(displayMetrics.widthPixels); + fullscreenSize.setHeight(displayMetrics.heightPixels); + } + } + return fullscreenSize; + } + + public static boolean isClass(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + public static boolean isIPv6(String address) { + try { + Inet6Address.getByName(address); + } catch (UnknownHostException e) { + return false; + } + return true; + } + + public static String normalizeIPv6(String address) throws Exception { + if (!Util.isIPv6(address)) { + throw new Exception("Invalid address: " + address); + } + return InetAddress.getByName(address).getCanonicalHostName(); + } + + public static T getOrDefault(Map map, String key, T defaultValue) { + return map.containsKey(key) ? (T) map.get(key) : defaultValue; + } + + @Nullable + public static byte[] readAllBytes(@Nullable InputStream inputStream) { + if (inputStream == null) { + return null; + } + + final int bufLen = 4 * 0x400; // 4KB + byte[] buf = new byte[bufLen]; + int readLen; + IOException exception = null; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] data = null; + + try { + while ((readLen = inputStream.read(buf, 0, bufLen)) != -1) + outputStream.write(buf, 0, readLen); + + data = outputStream.toByteArray(); + } catch (IOException e) { + exception = e; + } finally { + try { + inputStream.close(); + } catch (IOException e) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && exception != null) { + exception.addSuppressed(e); + } + } + try { + outputStream.close(); + } catch (IOException e) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && exception != null) { + exception.addSuppressed(e); + } + } + } + return data; + } + + @Nullable + public static Object invokeMethodIfExists(final O o, final String methodName, Object... args) { + Method[] methods = o.getClass().getMethods(); + for (Method method : methods) { + if (method.getName().equals(methodName)) { + try { + return method.invoke(o, args); + } catch (IllegalAccessException e) { + return null; + } catch (InvocationTargetException e) { + return null; + } + } + } + return null; + } + + public static Drawable drawableFromBytes(Context context, byte[] data) { + return new BitmapDrawable(context.getResources(), BitmapFactory.decodeByteArray(data, 0, data.length)); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/WebViewFeatureManager.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/WebViewFeatureManager.java new file mode 100644 index 0000000..f72c03c --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/WebViewFeatureManager.java @@ -0,0 +1,47 @@ +package com.pichillilorenzo.flutter_inappwebview_android; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class WebViewFeatureManager extends ChannelDelegateImpl { + protected static final String LOG_TAG = "WebViewFeatureManager"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_webviewfeature"; + + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public WebViewFeatureManager(@NonNull final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.plugin = plugin; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + switch (call.method) { + case "isFeatureSupported": + String feature = (String) call.argument("feature"); + result.success(WebViewFeature.isFeatureSupported(feature)); + break; + case "isStartupFeatureSupported": + if (plugin != null && plugin.activity != null) { + String startupFeature = (String) call.argument("startupFeature"); + result.success(WebViewFeature.isStartupFeatureSupported(plugin.activity, startupFeature)); + } + break; + default: + result.notImplemented(); + } + } + + @Override + public void dispose() { + super.dispose(); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ActionBroadcastReceiver.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ActionBroadcastReceiver.java new file mode 100644 index 0000000..cf160c7 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ActionBroadcastReceiver.java @@ -0,0 +1,47 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.browser.customtabs.CustomTabsIntent; + +public class ActionBroadcastReceiver extends BroadcastReceiver { + protected static final String LOG_TAG = "ActionBroadcastReceiver"; + public static final String KEY_ACTION_ID = "com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.ACTION_ID"; + public static final String KEY_ACTION_VIEW_ID = "com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.ACTION_VIEW_ID"; + public static final String KEY_ACTION_MANAGER_ID = "com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.ACTION_MANAGER_ID"; + public static final String KEY_URL_TITLE = "android.intent.extra.SUBJECT"; + + @Override + public void onReceive(Context context, Intent intent) { + int clickedId = intent.getIntExtra(CustomTabsIntent.EXTRA_REMOTEVIEWS_CLICKED_ID, -1); + String url = intent.getDataString(); + if (url != null) { + Bundle b = intent.getExtras(); + String viewId = b.getString(KEY_ACTION_VIEW_ID); + String managerId = b.getString(KEY_ACTION_MANAGER_ID); + + if (managerId != null) { + ChromeSafariBrowserManager chromeSafariBrowserManager = ChromeSafariBrowserManager.shared.get(managerId); + if (chromeSafariBrowserManager != null) { + if (clickedId == -1) { + int id = b.getInt(KEY_ACTION_ID); + String title = b.getString(KEY_URL_TITLE); + + ChromeCustomTabsActivity browser = chromeSafariBrowserManager.browsers.get(viewId); + if (browser != null && browser.channelDelegate != null) { + browser.channelDelegate.onItemActionPerform(id, url, title); + } + } else { + ChromeCustomTabsActivity browser = chromeSafariBrowserManager.browsers.get(viewId); + if (browser != null && browser.channelDelegate != null) { + browser.channelDelegate.onSecondaryItemActionPerform(browser.getResources().getResourceName(clickedId), url); + } + } + } + } + } + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivity.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivity.java new file mode 100644 index 0000000..444bc25 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivity.java @@ -0,0 +1,462 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.widget.RemoteViews; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.browser.customtabs.CustomTabColorSchemeParams; +import androidx.browser.customtabs.CustomTabsCallback; +import androidx.browser.customtabs.CustomTabsIntent; +import androidx.browser.customtabs.CustomTabsService; +import androidx.browser.customtabs.CustomTabsSession; +import androidx.browser.customtabs.EngagementSignalsCallback; + +import com.pichillilorenzo.flutter_inappwebview_android.R; +import com.pichillilorenzo.flutter_inappwebview_android.types.AndroidResource; +import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsActionButton; +import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsMenuItem; +import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsSecondaryToolbar; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +public class ChromeCustomTabsActivity extends Activity implements Disposable { + protected static final String LOG_TAG = "CustomTabsActivity"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_chromesafaribrowser_"; + + public String id; + @Nullable + public CustomTabsIntent.Builder builder; + public ChromeCustomTabsSettings customSettings = new ChromeCustomTabsSettings(); + public CustomTabActivityHelper customTabActivityHelper = new CustomTabActivityHelper(); + @Nullable + public CustomTabsSession customTabsSession; + public final static int CHROME_CUSTOM_TAB_REQUEST_CODE = 100; + public final static int NO_HISTORY_CHROME_CUSTOM_TAB_REQUEST_CODE = 101; + protected boolean onOpened = false; + protected boolean onCompletedInitialLoad = false; + protected boolean isBindSuccess = false; + @Nullable + public ChromeSafariBrowserManager manager; + @Nullable + public String initialUrl; + @Nullable + public List initialOtherLikelyURLs; + @Nullable + public Map initialHeaders; + @Nullable + public String initialReferrer; + public List menuItems = new ArrayList<>(); + @Nullable + public CustomTabsActionButton actionButton; + @Nullable + public CustomTabsSecondaryToolbar secondaryToolbar; + @Nullable + public ChromeCustomTabsChannelDelegate channelDelegate; + + @CallSuper + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.chrome_custom_tabs_layout); + + Bundle b = getIntent().getExtras(); + if (b == null) return; + + id = b.getString("id"); + + String managerId = b.getString("managerId"); + manager = ChromeSafariBrowserManager.shared.get(managerId); + if (manager == null || manager.plugin == null || manager.plugin.messenger == null) return; + + manager.browsers.put(id, this); + + MethodChannel channel = new MethodChannel(manager.plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id); + channelDelegate = new ChromeCustomTabsChannelDelegate(this, channel); + + initialUrl = b.getString("url"); + initialHeaders = (Map) b.getSerializable("headers"); + initialReferrer = b.getString("referrer"); + initialOtherLikelyURLs = b.getStringArrayList("otherLikelyURLs"); + + customSettings = new ChromeCustomTabsSettings(); + customSettings.parse((HashMap) b.getSerializable("settings")); + actionButton = CustomTabsActionButton.fromMap((Map) b.getSerializable("actionButton")); + secondaryToolbar = CustomTabsSecondaryToolbar.fromMap((Map) b.getSerializable("secondaryToolbar")); + List> menuItemList = (List>) b.getSerializable("menuItemList"); + for (Map menuItem : menuItemList) { + menuItems.add(CustomTabsMenuItem.fromMap(menuItem)); + } + + if (customSettings.noHistory && manager.plugin.noHistoryCustomTabsActivityCallbacks != null) { + manager.plugin.noHistoryCustomTabsActivityCallbacks.noHistoryBrowserIDs.put(id, id); + } + + final ChromeCustomTabsActivity chromeCustomTabsActivity = this; + + customTabActivityHelper.setConnectionCallback(new CustomTabActivityHelper.ConnectionCallback() { + @Override + public void onCustomTabsConnected() { + customTabsConnected(); + if (channelDelegate != null) { + channelDelegate.onServiceConnected(); + } + } + + @Override + public void onCustomTabsDisconnected() { + chromeCustomTabsActivity.close(); + dispose(); + } + }); + + customTabActivityHelper.setCustomTabsCallback(new CustomTabsCallback() { + @Override + public void onNavigationEvent(int navigationEvent, Bundle extras) { + if (navigationEvent == TAB_SHOWN && !onOpened) { + onOpened = true; + if (channelDelegate != null) { + channelDelegate.onOpened(); + } + } + + if (navigationEvent == NAVIGATION_FINISHED && !onCompletedInitialLoad) { + onCompletedInitialLoad = true; + if (channelDelegate != null) { + channelDelegate.onCompletedInitialLoad(); + } + } + + if (channelDelegate != null) { + channelDelegate.onNavigationEvent(navigationEvent); + } + } + + @Override + public void extraCallback(@NonNull String callbackName, Bundle args) { + } + + @Override + public void onMessageChannelReady(Bundle extras) { + if (channelDelegate != null) { + channelDelegate.onMessageChannelReady(); + } + } + + @Override + public void onPostMessage(@NonNull String message, Bundle extras) { + if (channelDelegate != null) { + channelDelegate.onPostMessage(message); + } + } + + @Override + public void onRelationshipValidationResult(@CustomTabsService.Relation int relation, + @NonNull Uri requestedOrigin, + boolean result, Bundle extras) { + if (channelDelegate != null) { + channelDelegate.onRelationshipValidationResult(relation, requestedOrigin, result); + } + } + }); + } + + public void launchUrl(@NonNull String url, + @Nullable Map headers, + @Nullable String referrer, + @Nullable List otherLikelyURLs) { + launchUrlWithSession(customTabsSession, url, headers, referrer, otherLikelyURLs); + } + + public void launchUrlWithSession(@Nullable CustomTabsSession session, + @NonNull String url, + @Nullable Map headers, + @Nullable String referrer, + @Nullable List otherLikelyURLs) { + mayLaunchUrl(url, otherLikelyURLs); + builder = new CustomTabsIntent.Builder(session); + prepareCustomTabs(); + + CustomTabsIntent customTabsIntent = builder.build(); + prepareCustomTabsIntent(customTabsIntent); + + CustomTabActivityHelper.openCustomTab(this, customTabsIntent, Uri.parse(url), headers, + referrer != null ? Uri.parse(referrer) : null, CHROME_CUSTOM_TAB_REQUEST_CODE); + } + + public boolean mayLaunchUrl(@Nullable String url, @Nullable List otherLikelyURLs) { + Uri uri = url != null ? Uri.parse(url) : null; + + List bundleOtherLikelyURLs = new ArrayList<>(); + if (otherLikelyURLs != null) { + Bundle bundleOtherLikelyURL = new Bundle(); + for (String otherLikelyURL : otherLikelyURLs) { + bundleOtherLikelyURL.putString(CustomTabsService.KEY_URL, otherLikelyURL); + } + } + return customTabActivityHelper.mayLaunchUrl(uri, null, bundleOtherLikelyURLs); + } + + @CallSuper + public void customTabsConnected() { + customTabsSession = customTabActivityHelper.getSession(); + + if (customTabsSession != null) { + try { + Bundle bundle = new Bundle(); + if (customTabsSession.isEngagementSignalsApiAvailable(bundle)) { + customTabsSession.setEngagementSignalsCallback(new EngagementSignalsCallback() { + @Override + public void onVerticalScrollEvent(boolean isDirectionUp, @NonNull Bundle extras) { + if (channelDelegate != null) { + channelDelegate.onVerticalScrollEvent(isDirectionUp); + } + } + + @Override + public void onGreatestScrollPercentageIncreased(int scrollPercentage, @NonNull Bundle extras) { + if (channelDelegate != null) { + channelDelegate.onGreatestScrollPercentageIncreased(scrollPercentage); + } + } + + @Override + public void onSessionEnded(boolean didUserInteract, @NonNull Bundle extras) { + if (channelDelegate != null) { + channelDelegate.onSessionEnded(didUserInteract); + } + } + }, bundle); + } + } catch (Throwable e) { + Log.d(LOG_TAG, "Custom Tabs Engagement Signals API not supported", e); + } + } + + // avoid webpage reopen if isBindSuccess is false: onServiceConnected->launchUrl + if (isBindSuccess && initialUrl != null) { + launchUrl(initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs); + } + } + + private void prepareCustomTabs() { + if (builder == null) { + return; + } + + if (customSettings.addDefaultShareMenuItem != null) { + builder.setShareState(customSettings.addDefaultShareMenuItem ? + CustomTabsIntent.SHARE_STATE_ON : CustomTabsIntent.SHARE_STATE_OFF); + } else { + builder.setShareState(customSettings.shareState); + } + + CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder(); + if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) { + defaultColorSchemeBuilder.setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor)); + } + if (customSettings.navigationBarColor != null && !customSettings.navigationBarColor.isEmpty()) { + defaultColorSchemeBuilder.setNavigationBarColor(Color.parseColor(customSettings.navigationBarColor)); + } + if (customSettings.navigationBarDividerColor != null && !customSettings.navigationBarDividerColor.isEmpty()) { + defaultColorSchemeBuilder.setNavigationBarDividerColor(Color.parseColor(customSettings.navigationBarDividerColor)); + } + if (customSettings.secondaryToolbarColor != null && !customSettings.secondaryToolbarColor.isEmpty()) { + defaultColorSchemeBuilder.setSecondaryToolbarColor(Color.parseColor(customSettings.secondaryToolbarColor)); + } + builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder.build()); + + builder.setShowTitle(customSettings.showTitle); + builder.setUrlBarHidingEnabled(customSettings.enableUrlBarHiding); + builder.setInstantAppsEnabled(customSettings.instantAppsEnabled); + if (customSettings.startAnimations.size() == 2) { + builder.setStartAnimations(this, + customSettings.startAnimations.get(0).getIdentifier(this), + customSettings.startAnimations.get(1).getIdentifier(this)); + } + if (customSettings.exitAnimations.size() == 2) { + builder.setExitAnimations(this, + customSettings.exitAnimations.get(0).getIdentifier(this), + customSettings.exitAnimations.get(1).getIdentifier(this)); + } + + for (CustomTabsMenuItem menuItem : menuItems) { + builder.addMenuItem(menuItem.getLabel(), + createPendingIntent(menuItem.getId())); + } + + if (actionButton != null) { + byte[] data = actionButton.getIcon(); + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inMutable = true; + Bitmap bmp = BitmapFactory.decodeByteArray( + data, 0, data.length, bitmapOptions + ); + builder.setActionButton(bmp, actionButton.getDescription(), + createPendingIntent(actionButton.getId()), + actionButton.isShouldTint()); + } + + if (secondaryToolbar != null) { + AndroidResource layout = secondaryToolbar.getLayout(); + RemoteViews remoteViews = new RemoteViews(layout.getDefPackage(), layout.getIdentifier(this)); + int[] clickableIDs = new int[secondaryToolbar.getClickableIDs().size()]; + for (int i = 0, length = secondaryToolbar.getClickableIDs().size(); i < length; i++) { + AndroidResource clickableID = secondaryToolbar.getClickableIDs().get(i); + clickableIDs[i] = clickableID.getIdentifier(this); + } + builder.setSecondaryToolbarViews(remoteViews, clickableIDs, getSecondaryToolbarOnClickPendingIntent()); + } + } + + public PendingIntent getSecondaryToolbarOnClickPendingIntent() { + Intent broadcastIntent = new Intent(this, ActionBroadcastReceiver.class); + + Bundle extras = new Bundle(); + extras.putString(ActionBroadcastReceiver.KEY_ACTION_VIEW_ID, id); + extras.putString(ActionBroadcastReceiver.KEY_ACTION_MANAGER_ID, manager != null ? manager.id : null); + broadcastIntent.putExtras(extras); + + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return PendingIntent.getBroadcast( + this, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + } else { + return PendingIntent.getBroadcast( + this, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + } + + private void prepareCustomTabsIntent(CustomTabsIntent customTabsIntent) { + if (customSettings.packageName != null) + customTabsIntent.intent.setPackage(customSettings.packageName); + else + customTabsIntent.intent.setPackage(CustomTabsHelper.getPackageNameToUse(this)); + + if (customSettings.keepAliveEnabled) + CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent); + + if (customSettings.alwaysUseBrowserUI) + CustomTabsIntent.setAlwaysUseBrowserUI(customTabsIntent.intent); + } + + public void updateActionButton(@NonNull byte[] icon, @NonNull String description) { + if (customTabsSession == null || actionButton == null) { + return; + } + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inMutable = true; + Bitmap bmp = BitmapFactory.decodeByteArray( + icon, 0, icon.length, bitmapOptions + ); + customTabsSession.setActionButton(bmp, description); + actionButton.setIcon(icon); + actionButton.setDescription(description); + } + + public void updateSecondaryToolbar(CustomTabsSecondaryToolbar secondaryToolbar) { + if (customTabsSession == null) { + return; + } + AndroidResource layout = secondaryToolbar.getLayout(); + RemoteViews remoteViews = new RemoteViews(layout.getDefPackage(), layout.getIdentifier(this)); + int[] clickableIDs = new int[secondaryToolbar.getClickableIDs().size()]; + for (int i = 0, length = secondaryToolbar.getClickableIDs().size(); i < length; i++) { + AndroidResource clickableID = secondaryToolbar.getClickableIDs().get(i); + clickableIDs[i] = clickableID.getIdentifier(this); + } + customTabsSession.setSecondaryToolbarViews(remoteViews, clickableIDs, getSecondaryToolbarOnClickPendingIntent()); + this.secondaryToolbar = secondaryToolbar; + } + + @Override + protected void onStart() { + super.onStart(); + isBindSuccess = customTabActivityHelper.bindCustomTabsService(this); + + if (!isBindSuccess && initialUrl != null) { + // chrome process not running, start tab directly + launchUrlWithSession(null, initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs); + } + } + + @Override + protected void onStop() { + super.onStop(); + customTabActivityHelper.unbindCustomTabsService(this); + isBindSuccess = false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == CHROME_CUSTOM_TAB_REQUEST_CODE) { + close(); + dispose(); + } + } + + private PendingIntent createPendingIntent(int actionSourceId) { + Intent actionIntent = new Intent(this, ActionBroadcastReceiver.class); + + Bundle extras = new Bundle(); + extras.putInt(ActionBroadcastReceiver.KEY_ACTION_ID, actionSourceId); + extras.putString(ActionBroadcastReceiver.KEY_ACTION_VIEW_ID, id); + extras.putString(ActionBroadcastReceiver.KEY_ACTION_MANAGER_ID, manager != null ? manager.id : null); + actionIntent.putExtras(extras); + + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return PendingIntent.getBroadcast( + this, actionSourceId, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + } else { + return PendingIntent.getBroadcast( + this, actionSourceId, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + } + + @Override + public void dispose() { + onStop(); + onDestroy(); + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + if (manager != null) { + if (manager.browsers.containsKey(id)) { + manager.browsers.put(id, null); + } + } + manager = null; + } + + public void close() { + onStop(); + onDestroy(); + customTabsSession = null; + finish(); + if (channelDelegate != null) { + channelDelegate.onClosed(); + } + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivitySingleInstance.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivitySingleInstance.java new file mode 100644 index 0000000..73d6de7 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivitySingleInstance.java @@ -0,0 +1,7 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +public class ChromeCustomTabsActivitySingleInstance extends ChromeCustomTabsActivity { + + protected static final String LOG_TAG = "ChromeCustomTabsActivitySingleInstance"; + +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsChannelDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsChannelDelegate.java new file mode 100644 index 0000000..703a337 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsChannelDelegate.java @@ -0,0 +1,251 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.browser.customtabs.CustomTabsService; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsSecondaryToolbar; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class ChromeCustomTabsChannelDelegate extends ChannelDelegateImpl { + @Nullable + private ChromeCustomTabsActivity chromeCustomTabsActivity; + + public ChromeCustomTabsChannelDelegate(@NonNull ChromeCustomTabsActivity chromeCustomTabsActivity, @NonNull MethodChannel channel) { + super(channel); + this.chromeCustomTabsActivity = chromeCustomTabsActivity; + } + + @Override + public void onMethodCall(@NonNull final MethodCall call, @NonNull final MethodChannel.Result result) { + switch (call.method) { + case "launchUrl": + if (chromeCustomTabsActivity != null) { + String url = (String) call.argument("url"); + if (url != null) { + Map headers = (Map) call.argument("headers"); + String referrer = (String) call.argument("referrer"); + List otherLikelyURLs = (List) call.argument("otherLikelyURLs"); + chromeCustomTabsActivity.launchUrl(url, headers, referrer, otherLikelyURLs); + result.success(true); + } else { + result.success(false); + } + } else { + result.success(false); + } + break; + case "mayLaunchUrl": + if (chromeCustomTabsActivity != null) { + String url = (String) call.argument("url"); + List otherLikelyURLs = (List) call.argument("otherLikelyURLs"); + result.success(chromeCustomTabsActivity.mayLaunchUrl(url, otherLikelyURLs)); + } else { + result.success(false); + } + break; + case "updateActionButton": + if (chromeCustomTabsActivity != null) { + byte[] icon = (byte[]) call.argument("icon"); + String description = (String) call.argument("description"); + chromeCustomTabsActivity.updateActionButton(icon, description); + result.success(true); + } else { + result.success(false); + } + break; + case "validateRelationship": + if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) { + Integer relation = (Integer) call.argument("relation"); + String origin = (String) call.argument("origin"); + result.success(chromeCustomTabsActivity.customTabsSession.validateRelationship(relation, Uri.parse(origin), null)); + } else { + result.success(false); + } + break; + case "updateSecondaryToolbar": + if (chromeCustomTabsActivity != null) { + CustomTabsSecondaryToolbar secondaryToolbar = CustomTabsSecondaryToolbar.fromMap((Map) call.argument("secondaryToolbar")); + chromeCustomTabsActivity.updateSecondaryToolbar(secondaryToolbar); + result.success(true); + } else { + result.success(false); + } + break; + case "requestPostMessageChannel": + if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) { + String sourceOrigin = (String) call.argument("sourceOrigin"); + String targetOrigin = (String) call.argument("targetOrigin"); + result.success(chromeCustomTabsActivity.customTabsSession.requestPostMessageChannel(Uri.parse(sourceOrigin), + targetOrigin != null ? Uri.parse(targetOrigin) : null, new Bundle())); + } else { + result.success(false); + } + break; + case "postMessage": + if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) { + String message = (String) call.argument("message"); + result.success(chromeCustomTabsActivity.customTabsSession.postMessage(message, new Bundle())); + } else { + result.success(CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR); + } + break; + case "isEngagementSignalsApiAvailable": + if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) { + try { + result.success(chromeCustomTabsActivity.customTabsSession.isEngagementSignalsApiAvailable(new Bundle())); + } catch (Throwable e) { + result.success(false); + } + } else { + result.success(false); + } + break; + case "close": + if (chromeCustomTabsActivity != null) { + chromeCustomTabsActivity.onStop(); + chromeCustomTabsActivity.onDestroy(); + chromeCustomTabsActivity.close(); + + if (chromeCustomTabsActivity.manager != null && chromeCustomTabsActivity.manager.plugin != null && + chromeCustomTabsActivity.manager.plugin.activity != null) { + Activity activity = chromeCustomTabsActivity.manager.plugin.activity; + // https://stackoverflow.com/a/41596629/4637638 + Intent myIntent = new Intent(activity, activity.getClass()); + myIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + myIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + activity.startActivity(myIntent); + } + chromeCustomTabsActivity.dispose(); + result.success(true); + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + public void onServiceConnected() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onServiceConnected", obj); + } + + public void onOpened() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onOpened", obj); + } + + public void onCompletedInitialLoad() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onCompletedInitialLoad", obj); + } + + public void onNavigationEvent(int navigationEvent) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("navigationEvent", navigationEvent); + channel.invokeMethod("onNavigationEvent", obj); + } + + public void onClosed() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onClosed", obj); + } + + public void onItemActionPerform(int id, String url, String title) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("id", id); + obj.put("url", url); + obj.put("title", title); + channel.invokeMethod("onItemActionPerform", obj); + } + + public void onSecondaryItemActionPerform(String name, String url) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("name", name); + obj.put("url", url); + channel.invokeMethod("onSecondaryItemActionPerform", obj); + } + + public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin, boolean result) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("relation", relation); + obj.put("requestedOrigin", requestedOrigin.toString()); + obj.put("result", result); + channel.invokeMethod("onRelationshipValidationResult", obj); + } + + public void onMessageChannelReady() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onMessageChannelReady", obj); + } + + public void onPostMessage(@NonNull String message) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("message", message); + channel.invokeMethod("onPostMessage", obj); + } + + public void onVerticalScrollEvent(boolean isDirectionUp) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("isDirectionUp", isDirectionUp); + channel.invokeMethod("onVerticalScrollEvent", obj); + } + + public void onGreatestScrollPercentageIncreased(int scrollPercentage) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("scrollPercentage", scrollPercentage); + channel.invokeMethod("onGreatestScrollPercentageIncreased", obj); + } + + public void onSessionEnded(boolean didUserInteract) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("didUserInteract", didUserInteract); + channel.invokeMethod("onSessionEnded", obj); + } + + @Override + public void dispose() { + super.dispose(); + chromeCustomTabsActivity = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsSettings.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsSettings.java new file mode 100644 index 0000000..b512cf4 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsSettings.java @@ -0,0 +1,187 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.browser.customtabs.CustomTabsIntent; +import androidx.browser.trusted.ScreenOrientation; +import androidx.browser.trusted.TrustedWebActivityDisplayMode; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; +import com.pichillilorenzo.flutter_inappwebview_android.types.AndroidResource; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ChromeCustomTabsSettings implements ISettings { + + final static String LOG_TAG = "ChromeCustomTabsSettings"; + + @Deprecated + public Boolean addDefaultShareMenuItem; + public Integer shareState = CustomTabsIntent.SHARE_STATE_DEFAULT; + public Boolean showTitle = true; + @Nullable + public String toolbarBackgroundColor; + @Nullable + public String navigationBarColor; + @Nullable + public String navigationBarDividerColor; + @Nullable + public String secondaryToolbarColor; + public Boolean enableUrlBarHiding = false; + public Boolean instantAppsEnabled = false; + public String packageName; + public Boolean keepAliveEnabled = false; + public Boolean isSingleInstance = false; + public Boolean noHistory = false; + public Boolean isTrustedWebActivity = false; + public List additionalTrustedOrigins = new ArrayList<>(); + public TrustedWebActivityDisplayMode displayMode = null; + public Integer screenOrientation = ScreenOrientation.DEFAULT; + public List startAnimations = new ArrayList<>(); + public List exitAnimations = new ArrayList<>(); + public Boolean alwaysUseBrowserUI = false; + + @NonNull + @Override + public ChromeCustomTabsSettings parse(@NonNull Map options) { + for (Map.Entry pair : options.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "addDefaultShareMenuItem": + addDefaultShareMenuItem = (Boolean) value; + break; + case "shareState": + shareState = (Integer) value; + break; + case "showTitle": + showTitle = (Boolean) value; + break; + case "toolbarBackgroundColor": + toolbarBackgroundColor = (String) value; + break; + case "navigationBarColor": + navigationBarColor = (String) value; + break; + case "navigationBarDividerColor": + navigationBarDividerColor = (String) value; + break; + case "secondaryToolbarColor": + secondaryToolbarColor = (String) value; + break; + case "enableUrlBarHiding": + enableUrlBarHiding = (Boolean) value; + break; + case "instantAppsEnabled": + instantAppsEnabled = (Boolean) value; + break; + case "packageName": + packageName = (String) value; + break; + case "keepAliveEnabled": + keepAliveEnabled = (Boolean) value; + break; + case "isSingleInstance": + isSingleInstance = (Boolean) value; + break; + case "noHistory": + noHistory = (Boolean) value; + break; + case "isTrustedWebActivity": + isTrustedWebActivity = (Boolean) value; + break; + case "additionalTrustedOrigins": + additionalTrustedOrigins = (List) value; + break; + case "displayMode": + Map displayModeMap = (Map) value; + String displayModeType = (String) displayModeMap.get("type"); + if (displayModeType != null) { + switch (displayModeType) { + case "IMMERSIVE_MODE": + boolean isSticky = (boolean) displayModeMap.get("isSticky"); + int layoutInDisplayCutoutMode = (int) displayModeMap.get("displayCutoutMode"); + displayMode = new TrustedWebActivityDisplayMode.ImmersiveMode(isSticky, layoutInDisplayCutoutMode); + break; + case "DEFAULT_MODE": + displayMode = new TrustedWebActivityDisplayMode.DefaultMode(); + break; + } + } + break; + case "screenOrientation": + screenOrientation = (Integer) value; + break; + case "startAnimations": + List> startAnimationsList = (List>) value; + for (Map startAnimation : startAnimationsList) { + AndroidResource androidResource = AndroidResource.fromMap(startAnimation); + if (androidResource != null) { + startAnimations.add(AndroidResource.fromMap(startAnimation)); + } + } + break; + case "exitAnimations": + List> exitAnimationsList = (List>) value; + for (Map exitAnimation : exitAnimationsList) { + AndroidResource androidResource = AndroidResource.fromMap(exitAnimation); + if (androidResource != null) { + exitAnimations.add(AndroidResource.fromMap(exitAnimation)); + } + } + break; + case "alwaysUseBrowserUI": + alwaysUseBrowserUI = (Boolean) value; + break; + } + } + + return this; + } + + @NonNull + @Override + public Map toMap() { + Map options = new HashMap<>(); + options.put("addDefaultShareMenuItem", addDefaultShareMenuItem); + options.put("showTitle", showTitle); + options.put("toolbarBackgroundColor", toolbarBackgroundColor); + options.put("navigationBarColor", navigationBarColor); + options.put("navigationBarDividerColor", navigationBarDividerColor); + options.put("secondaryToolbarColor", secondaryToolbarColor); + options.put("enableUrlBarHiding", enableUrlBarHiding); + options.put("instantAppsEnabled", instantAppsEnabled); + options.put("packageName", packageName); + options.put("keepAliveEnabled", keepAliveEnabled); + options.put("isSingleInstance", isSingleInstance); + options.put("noHistory", noHistory); + options.put("isTrustedWebActivity", isTrustedWebActivity); + options.put("additionalTrustedOrigins", additionalTrustedOrigins); + options.put("screenOrientation", screenOrientation); + options.put("alwaysUseBrowserUI", alwaysUseBrowserUI); + return options; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull ChromeCustomTabsActivity chromeCustomTabsActivity) { + Map realOptions = toMap(); + if (chromeCustomTabsActivity != null) { + Intent intent = chromeCustomTabsActivity.getIntent(); + if (intent != null) { + realOptions.put("packageName", intent.getPackage()); + realOptions.put("keepAliveEnabled", intent.hasExtra(CustomTabsHelper.EXTRA_CUSTOM_TABS_KEEP_ALIVE)); + } + } + return realOptions; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeSafariBrowserManager.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeSafariBrowserManager.java new file mode 100644 index 0000000..1fab279 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeSafariBrowserManager.java @@ -0,0 +1,139 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.browser.customtabs.CustomTabsClient; +import androidx.browser.customtabs.CustomTabsIntent; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class ChromeSafariBrowserManager extends ChannelDelegateImpl { + protected static final String LOG_TAG = "ChromeBrowserManager"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_chromesafaribrowser"; + + @Nullable + public InAppWebViewFlutterPlugin plugin; + public String id; + public static final Map shared = new HashMap<>(); + public final Map browsers = new HashMap<>(); + + public ChromeSafariBrowserManager(final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.id = UUID.randomUUID().toString(); + this.plugin = plugin; + shared.put(this.id, this); + } + + @Override + public void onMethodCall(final MethodCall call, final MethodChannel.Result result) { + final String viewId = (String) call.argument("id"); + + switch (call.method) { + case "open": + if (plugin != null && plugin.activity != null) { + String url = (String) call.argument("url"); + HashMap headers = (HashMap) call.argument("headers"); + String referrer = (String) call.argument("referrer"); + ArrayList otherLikelyURLs = (ArrayList) call.argument("otherLikelyURLs"); + HashMap settings = (HashMap) call.argument("settings"); + HashMap actionButton = (HashMap) call.argument("actionButton"); + HashMap secondaryToolbar = (HashMap) call.argument("secondaryToolbar"); + List> menuItemList = (List>) call.argument("menuItemList"); + open(plugin.activity, viewId, url, headers, referrer, otherLikelyURLs, settings, actionButton, secondaryToolbar, menuItemList, result); + } else { + result.success(false); + } + break; + case "isAvailable": + if (plugin != null && plugin.activity != null) { + result.success(CustomTabActivityHelper.isAvailable(plugin.activity)); + } else { + result.success(false); + } + break; + case "getMaxToolbarItems": + result.success(CustomTabsIntent.getMaxToolbarItems()); + break; + case "getPackageName": + if (plugin != null && plugin.activity != null) { + ArrayList packages = (ArrayList) call.argument("packages"); + Boolean ignoreDefault = (Boolean) call.argument("ignoreDefault"); + result.success(CustomTabsClient.getPackageName(plugin.activity, packages, ignoreDefault)); + } else { + result.success(null); + } + break; + default: + result.notImplemented(); + } + } + + public void open(Activity activity, String viewId, @Nullable String url, @Nullable HashMap headers, + @Nullable String referrer, @Nullable ArrayList otherLikelyURLs, + HashMap settings, HashMap actionButton, + HashMap secondaryToolbar, + List> menuItemList, MethodChannel.Result result) { + + Intent intent = null; + Bundle extras = new Bundle(); + extras.putString("url", url); + extras.putString("id", viewId); + extras.putString("managerId", this.id); + extras.putSerializable("headers", headers); + extras.putString("referrer", referrer); + extras.putSerializable("otherLikelyURLs", otherLikelyURLs); + extras.putSerializable("settings", settings); + extras.putSerializable("actionButton", (Serializable) actionButton); + extras.putSerializable("secondaryToolbar", (Serializable) secondaryToolbar); + extras.putSerializable("menuItemList", (Serializable) menuItemList); + + Boolean isSingleInstance = Util.getOrDefault(settings, "isSingleInstance", false); + Boolean isTrustedWebActivity = Util.getOrDefault(settings, "isTrustedWebActivity", false); + if (CustomTabActivityHelper.isAvailable(activity)) { + intent = new Intent(activity, !isSingleInstance ? + (!isTrustedWebActivity ? ChromeCustomTabsActivity.class : TrustedWebActivity.class) : + (!isTrustedWebActivity ? ChromeCustomTabsActivitySingleInstance.class : TrustedWebActivitySingleInstance.class)); + intent.putExtras(extras); + Boolean noHistory = Util.getOrDefault(settings, "noHistory", false); + if (noHistory) { + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + } + activity.startActivity(intent); + result.success(true); + return; + } + + result.error(LOG_TAG, "ChromeCustomTabs is not available!", null); + } + + @Override + public void dispose() { + super.dispose(); + Collection browserList = browsers.values(); + for (ChromeCustomTabsActivity browser : browserList) { + if (browser != null) { + browser.close(); + browser.dispose(); + } + } + browsers.clear(); + shared.remove(this.id); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabActivityHelper.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabActivityHelper.java new file mode 100644 index 0000000..61d759c --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabActivityHelper.java @@ -0,0 +1,177 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Browser; + +import androidx.annotation.Nullable; +import androidx.browser.customtabs.CustomTabsCallback; +import androidx.browser.customtabs.CustomTabsClient; +import androidx.browser.customtabs.CustomTabsIntent; +import androidx.browser.customtabs.CustomTabsServiceConnection; +import androidx.browser.customtabs.CustomTabsSession; +import androidx.browser.trusted.TrustedWebActivityIntent; + +import java.util.List; +import java.util.Map; + +/** + * This is a helper class to manage the connection to the Custom Tabs Service. + */ +public class CustomTabActivityHelper implements ServiceConnectionCallback { + private CustomTabsSession mCustomTabsSession; + private CustomTabsClient mClient; + private CustomTabsServiceConnection mConnection; + private ConnectionCallback mConnectionCallback; + private CustomTabsCallback mCustomTabsCallback; + + /** + * Opens the URL on a Custom Tab if possible. + * + * @param activity The host activity. + * @param intent a intent to be used if Custom Tabs is available. + * @param uri the Uri to be opened. + */ + public static void openCustomTab(Activity activity, + Intent intent, + Uri uri, + @Nullable Map headers, + @Nullable Uri referrer, + int requestCode) { + intent.setData(uri); + if (headers != null) { + Bundle bundleHeaders = new Bundle(); + for (Map.Entry header : headers.entrySet()) { + bundleHeaders.putString(header.getKey(), header.getValue()); + } + intent.putExtra(Browser.EXTRA_HEADERS, bundleHeaders); + } + if (referrer != null) { + intent.putExtra(Intent.EXTRA_REFERRER, referrer); + } + activity.startActivityForResult(intent, requestCode); + } + + public static void openCustomTab(Activity activity, + CustomTabsIntent customTabsIntent, + Uri uri, + @Nullable Map headers, + @Nullable Uri referrer, + int requestCode) { + CustomTabActivityHelper.openCustomTab(activity, customTabsIntent.intent, uri, + headers, referrer, requestCode); + } + + public static void openTrustedWebActivity(Activity activity, + TrustedWebActivityIntent trustedWebActivityIntent, + Uri uri, + @Nullable Map headers, + @Nullable Uri referrer, + int requestCode) { + CustomTabActivityHelper.openCustomTab(activity, trustedWebActivityIntent.getIntent(), uri, + headers, referrer, requestCode); + } + + + public static boolean isAvailable(Activity activity) { + return CustomTabsHelper.getPackageNameToUse(activity) != null; + } + + /** + * Unbinds the Activity from the Custom Tabs Service. + * @param activity the activity that is connected to the service. + */ + public void unbindCustomTabsService(Activity activity) { + if (mConnection == null) return; + activity.unbindService(mConnection); + mClient = null; + mCustomTabsSession = null; + mConnection = null; + } + + /** + * Creates or retrieves an exiting CustomTabsSession. + * + * @return a CustomTabsSession. + */ + @Nullable + public CustomTabsSession getSession() { + if (mClient == null) { + mCustomTabsSession = null; + } else if (mCustomTabsSession == null) { + mCustomTabsSession = mClient.newSession(mCustomTabsCallback); + } + return mCustomTabsSession; + } + + /** + * Register a Callback to be called when connected or disconnected from the Custom Tabs Service. + * @param connectionCallback + */ + public void setConnectionCallback(ConnectionCallback connectionCallback) { + this.mConnectionCallback = connectionCallback; + } + + public void setCustomTabsCallback(CustomTabsCallback customTabsCallback) { + this.mCustomTabsCallback = customTabsCallback; + } + + /** + * Binds the Activity to the Custom Tabs Service. + * @param activity the activity to be binded to the service. + */ + public boolean bindCustomTabsService(Activity activity) { + if (mClient != null) return true; + + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + if (packageName == null) return false; + + mConnection = new ServiceConnection(this); + return CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection); + } + + /** + * @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}. + * @return true if call to mayLaunchUrl was accepted. + */ + public boolean mayLaunchUrl(Uri uri, Bundle extras, List otherLikelyBundles) { + if (mClient == null) return false; + + CustomTabsSession session = getSession(); + if (session == null) return false; + + return session.mayLaunchUrl(uri, extras, otherLikelyBundles); + } + + @Override + public void onServiceConnected(CustomTabsClient client) { + mClient = client; + mClient.warmup(0L); + if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected(); + } + + @Override + public void onServiceDisconnected() { + mClient = null; + mCustomTabsSession = null; + if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected(); + } + + /** + * A Callback for when the service is connected or disconnected. Use those callbacks to + * handle UI changes when the service is connected or disconnected. + */ + public interface ConnectionCallback { + /** + * Called when the service is connected. + */ + void onCustomTabsConnected(); + + /** + * Called when the service is disconnected. + */ + void onCustomTabsDisconnected(); + } +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabsHelper.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabsHelper.java new file mode 100644 index 0000000..6e3a216 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabsHelper.java @@ -0,0 +1,134 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +import androidx.browser.customtabs.CustomTabsService; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for Custom Tabs. + */ +public class CustomTabsHelper { + protected static final String TAG = "CustomTabsHelper"; + static final String STABLE_PACKAGE = "com.android.chrome"; + static final String BETA_PACKAGE = "com.chrome.beta"; + static final String DEV_PACKAGE = "com.chrome.dev"; + static final String LOCAL_PACKAGE = "com.google.android.apps.chrome"; + protected static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = + "android.support.customtabs.extra.KEEP_ALIVE"; + + private static String sPackageNameToUse; + + private CustomTabsHelper() {} + + public static void addKeepAliveExtra(Context context, Intent intent) { + Intent keepAliveIntent = new Intent().setClassName( + context.getPackageName(), KeepAliveService.class.getCanonicalName()); + intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent); + } + + /** + * Goes through all apps that handle VIEW intents and have a warmup service. Picks + * the one chosen by the user if there is one, otherwise makes a best effort to return a + * valid package name. + * + * This is not threadsafe. + * + * @param context {@link Context} to use for accessing {@link PackageManager}. + * @return The package name recommended to use for connecting to custom tabs related components. + */ + public static String getPackageNameToUse(Context context) { + if (sPackageNameToUse != null) return sPackageNameToUse; + + PackageManager pm = context.getPackageManager(); + // Get default VIEW intent handler. + Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); + activityIntent.addCategory(Intent.CATEGORY_BROWSABLE); + ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0); + String defaultViewHandlerPackageName = null; + if (defaultViewHandlerInfo != null) { + defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName; + } + + // Get all apps that can handle VIEW intents. + int flags = 0; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags |= PackageManager.MATCH_ALL; + } + List resolvedActivityList = pm.queryIntentActivities(activityIntent, flags); + List packagesSupportingCustomTabs = new ArrayList<>(); + for (ResolveInfo info : resolvedActivityList) { + Intent serviceIntent = new Intent(); + serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION); + serviceIntent.setPackage(info.activityInfo.packageName); + if (pm.resolveService(serviceIntent, 0) != null) { + packagesSupportingCustomTabs.add(info.activityInfo.packageName); + } + } + + // Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents + // and service calls. + if (packagesSupportingCustomTabs.isEmpty()) { + sPackageNameToUse = null; + } else if (packagesSupportingCustomTabs.size() == 1) { + sPackageNameToUse = packagesSupportingCustomTabs.get(0); + } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) + && !hasSpecializedHandlerIntents(context, activityIntent) + && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) { + sPackageNameToUse = defaultViewHandlerPackageName; + } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) { + sPackageNameToUse = STABLE_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) { + sPackageNameToUse = BETA_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) { + sPackageNameToUse = DEV_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) { + sPackageNameToUse = LOCAL_PACKAGE; + } + return sPackageNameToUse; + } + + /** + * Used to check whether there is a specialized handler for a given intent. + * @param intent The intent to check with. + * @return Whether there is a specialized handler for the given intent. + */ + private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) { + try { + PackageManager pm = context.getPackageManager(); + List handlers = pm.queryIntentActivities( + intent, + PackageManager.GET_RESOLVED_FILTER); + if (handlers == null || handlers.size() == 0) { + return false; + } + for (ResolveInfo resolveInfo : handlers) { + IntentFilter filter = resolveInfo.filter; + if (filter == null) continue; + if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue; + if (resolveInfo.activityInfo == null) continue; + return true; + } + } catch (RuntimeException e) { + Log.e(TAG, "Runtime exception while getting specialized handlers"); + } + return false; + } + + /** + * @return All possible chrome package names that provide custom tabs feature. + */ + public static String[] getPackages() { + return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE}; + } +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/KeepAliveService.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/KeepAliveService.java new file mode 100644 index 0000000..87dc3af --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/KeepAliveService.java @@ -0,0 +1,18 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; + +/** + * Empty service used by the custom tab to bind to, raising the application's importance. + */ +public class KeepAliveService extends Service { + private static final Binder sBinder = new Binder(); + + @Override + public IBinder onBind(Intent intent) { + return sBinder; + } +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/NoHistoryCustomTabsActivityCallbacks.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/NoHistoryCustomTabsActivityCallbacks.java new file mode 100644 index 0000000..3ada33e --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/NoHistoryCustomTabsActivityCallbacks.java @@ -0,0 +1,77 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import io.flutter.embedding.android.FlutterActivity; + +public class NoHistoryCustomTabsActivityCallbacks implements Disposable { + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public final Map noHistoryBrowserIDs = new HashMap<>(); + + public NoHistoryCustomTabsActivityCallbacks(@NonNull final InAppWebViewFlutterPlugin plugin) { + this.plugin = plugin; + } + + public Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + } + + @Override + public void onActivityResumed(@NonNull Activity activity) { + if (activity instanceof FlutterActivity && plugin != null && plugin.chromeSafariBrowserManager != null) { + Collection browserIds = noHistoryBrowserIDs.values(); + for (String browserId : browserIds) { + if (browserId != null) { + noHistoryBrowserIDs.put(browserId, null); + ChromeCustomTabsActivity browser = plugin.chromeSafariBrowserManager.browsers.get(browserId); + if (browser != null) { + browser.close(); + browser.dispose(); + } + } + } + } + } + + @Override + public void onActivityPaused(@NonNull Activity activity) { + } + + @Override + public void onActivityStopped(@NonNull Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { + } + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + } + }; + + @Override + public void dispose() { + noHistoryBrowserIDs.clear(); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnection.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnection.java new file mode 100644 index 0000000..de23615 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnection.java @@ -0,0 +1,32 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import android.content.ComponentName; +import androidx.browser.customtabs.CustomTabsClient; +import androidx.browser.customtabs.CustomTabsServiceConnection; + +import java.lang.ref.WeakReference; + +/** + * Implementation for the CustomTabsServiceConnection that avoids leaking the + * ServiceConnectionCallback + */ +public class ServiceConnection extends CustomTabsServiceConnection { + // A weak reference to the ServiceConnectionCallback to avoid leaking it. + private WeakReference mConnectionCallback; + + public ServiceConnection(ServiceConnectionCallback connectionCallback) { + mConnectionCallback = new WeakReference<>(connectionCallback); + } + + @Override + public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { + ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); + if (connectionCallback != null) connectionCallback.onServiceConnected(client); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); + if (connectionCallback != null) connectionCallback.onServiceDisconnected(); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnectionCallback.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnectionCallback.java new file mode 100644 index 0000000..3212b2f --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnectionCallback.java @@ -0,0 +1,19 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import androidx.browser.customtabs.CustomTabsClient; + +/** + * Callback for events when connecting and disconnecting from Custom Tabs Service. + */ +public interface ServiceConnectionCallback { + /** + * Called when the service is connected. + * @param client a CustomTabsClient + */ + void onServiceConnected(CustomTabsClient client); + + /** + * Called when the service is disconnected. + */ + void onServiceDisconnected(); +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivity.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivity.java new file mode 100644 index 0000000..d5f7015 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivity.java @@ -0,0 +1,84 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +import android.content.Intent; +import android.graphics.Color; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.browser.customtabs.CustomTabColorSchemeParams; +import androidx.browser.customtabs.CustomTabsIntent; +import androidx.browser.trusted.TrustedWebActivityIntent; +import androidx.browser.trusted.TrustedWebActivityIntentBuilder; + +import java.util.List; +import java.util.Map; + +public class TrustedWebActivity extends ChromeCustomTabsActivity { + + protected static final String LOG_TAG = "TrustedWebActivity"; + + public TrustedWebActivityIntentBuilder builder; + + @Override + public void launchUrl(@NonNull String url, + @Nullable Map headers, + @Nullable String referrer, + @Nullable List otherLikelyURLs) { + if (customTabsSession == null) { + return; + } + Uri uri = Uri.parse(url); + + mayLaunchUrl(url, otherLikelyURLs); + builder = new TrustedWebActivityIntentBuilder(uri); + prepareCustomTabs(); + + TrustedWebActivityIntent trustedWebActivityIntent = builder.build(customTabsSession); + prepareCustomTabsIntent(trustedWebActivityIntent); + + CustomTabActivityHelper.openTrustedWebActivity(this, trustedWebActivityIntent, uri, headers, + referrer != null ? Uri.parse(referrer) : null, CHROME_CUSTOM_TAB_REQUEST_CODE); + } + + private void prepareCustomTabs() { + CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder(); + if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) { + defaultColorSchemeBuilder.setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor)); + } + if (customSettings.navigationBarColor != null && !customSettings.navigationBarColor.isEmpty()) { + defaultColorSchemeBuilder.setNavigationBarColor(Color.parseColor(customSettings.navigationBarColor)); + } + if (customSettings.navigationBarDividerColor != null && !customSettings.navigationBarDividerColor.isEmpty()) { + defaultColorSchemeBuilder.setNavigationBarDividerColor(Color.parseColor(customSettings.navigationBarDividerColor)); + } + if (customSettings.secondaryToolbarColor != null && !customSettings.secondaryToolbarColor.isEmpty()) { + defaultColorSchemeBuilder.setSecondaryToolbarColor(Color.parseColor(customSettings.secondaryToolbarColor)); + } + builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder.build()); + + if (customSettings.additionalTrustedOrigins != null && !customSettings.additionalTrustedOrigins.isEmpty()) { + builder.setAdditionalTrustedOrigins(customSettings.additionalTrustedOrigins); + } + + if (customSettings.displayMode != null) { + builder.setDisplayMode(customSettings.displayMode); + } + + builder.setScreenOrientation(customSettings.screenOrientation); + } + + private void prepareCustomTabsIntent(TrustedWebActivityIntent trustedWebActivityIntent) { + Intent intent = trustedWebActivityIntent.getIntent(); + if (customSettings.packageName != null) + intent.setPackage(customSettings.packageName); + else + intent.setPackage(CustomTabsHelper.getPackageNameToUse(this)); + + if (customSettings.keepAliveEnabled) + CustomTabsHelper.addKeepAliveExtra(this, intent); + + if (customSettings.alwaysUseBrowserUI) + CustomTabsIntent.setAlwaysUseBrowserUI(intent); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivitySingleInstance.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivitySingleInstance.java new file mode 100644 index 0000000..d77c678 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivitySingleInstance.java @@ -0,0 +1,7 @@ +package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs; + +public class TrustedWebActivitySingleInstance extends TrustedWebActivity { + + protected static final String LOG_TAG = "TrustedWebActivitySingleInstance"; + +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlocker.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlocker.java new file mode 100644 index 0000000..5542196 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlocker.java @@ -0,0 +1,59 @@ +package com.pichillilorenzo.flutter_inappwebview_android.content_blocker; + +import androidx.annotation.NonNull; + +public class ContentBlocker { + @NonNull + private ContentBlockerTrigger trigger; + @NonNull + private ContentBlockerAction action; + + public ContentBlocker (@NonNull ContentBlockerTrigger trigger, @NonNull ContentBlockerAction action) { + this.trigger = trigger; + this.action = action; + } + + @NonNull + public ContentBlockerTrigger getTrigger() { + return trigger; + } + + public void setTrigger(@NonNull ContentBlockerTrigger trigger) { + this.trigger = trigger; + } + + @NonNull + public ContentBlockerAction getAction() { + return action; + } + + public void setAction(@NonNull ContentBlockerAction action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ContentBlocker that = (ContentBlocker) o; + + if (!trigger.equals(that.trigger)) return false; + return action.equals(that.action); + } + + @Override + public int hashCode() { + int result = trigger.hashCode(); + result = 31 * result + action.hashCode(); + return result; + } + + @Override + public String toString() { + return "ContentBlocker{" + + "trigger=" + trigger + + ", action=" + action + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerAction.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerAction.java new file mode 100644 index 0000000..1c2f38b --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerAction.java @@ -0,0 +1,71 @@ +package com.pichillilorenzo.flutter_inappwebview_android.content_blocker; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class ContentBlockerAction { + @NonNull + private ContentBlockerActionType type; + + @Nullable + private String selector; + + ContentBlockerAction(@NonNull ContentBlockerActionType type, @Nullable String selector) { + this.type = type; + if (this.type.equals(ContentBlockerActionType.CSS_DISPLAY_NONE)) { + assert(selector != null); + } + this.selector = selector; + } + + public static ContentBlockerAction fromMap(Map map) { + ContentBlockerActionType type = ContentBlockerActionType.fromValue((String) map.get("type")); + String selector = (String) map.get("selector"); + return new ContentBlockerAction(type, selector); + } + + @NonNull + public ContentBlockerActionType getType() { + return type; + } + + public void setType(@NonNull ContentBlockerActionType type) { + this.type = type; + } + + public String getSelector() { + return selector; + } + + public void setSelector(String selector) { + this.selector = selector; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ContentBlockerAction that = (ContentBlockerAction) o; + + if (type != that.type) return false; + return selector != null ? selector.equals(that.selector) : that.selector == null; + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + (selector != null ? selector.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ContentBlockerAction{" + + "type=" + type + + ", selector='" + selector + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerActionType.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerActionType.java new file mode 100644 index 0000000..89825a2 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerActionType.java @@ -0,0 +1,30 @@ +package com.pichillilorenzo.flutter_inappwebview_android.content_blocker; + +public enum ContentBlockerActionType { + BLOCK ("block"), + CSS_DISPLAY_NONE ("css-display-none"), + MAKE_HTTPS ("make-https"); + + private final String value; + + private ContentBlockerActionType(String value) { + this.value = value; + } + + public boolean equalsValue(String otherValue) { + return value.equals(otherValue); + } + + public static ContentBlockerActionType fromValue(String value) { + for( ContentBlockerActionType type : ContentBlockerActionType.values()) { + if(value.equals(type.value)) + return type; + } + throw new IllegalArgumentException("No enum constant: " + value); + } + + @Override + public String toString() { + return this.value; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerHandler.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerHandler.java new file mode 100644 index 0000000..8564dbe --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerHandler.java @@ -0,0 +1,336 @@ +package com.pichillilorenzo.flutter_inappwebview_android.content_blocker; + +import android.os.Build; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.WebResourceResponse; + +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.regex.Matcher; + +import javax.net.ssl.SSLHandshakeException; + +public class ContentBlockerHandler { + protected static final String LOG_TAG = "ContentBlockerHandler"; + + protected List ruleList = new ArrayList<>(); + + public ContentBlockerHandler() {} + + public ContentBlockerHandler(List ruleList) { + this.ruleList = ruleList; + } + + public List getRuleList() { + return this.ruleList; + } + + public void setRuleList(List newRuleList) { + this.ruleList = newRuleList; + } + + @Nullable + public WebResourceResponse checkUrl(final InAppWebView webView, WebResourceRequestExt request, + ContentBlockerTriggerResourceType responseResourceType) + throws URISyntaxException, InterruptedException, MalformedURLException { + if (webView.customSettings.contentBlockers == null) + return null; + + String url = request.getUrl(); + + URI u; + try { + u = new URI(url); + } catch (URISyntaxException e) { + String[] urlSplit = url.split(":"); + String scheme = urlSplit[0]; + URL tempUrl = new URL(url.replace(scheme, "https")); + u = new URI(scheme, tempUrl.getUserInfo(), tempUrl.getHost(), tempUrl.getPort(), tempUrl.getPath(), tempUrl.getQuery(), tempUrl.getRef()); + } + String host = u.getHost(); + int port = u.getPort(); + String scheme = u.getScheme(); + // thread safe copy list + List ruleListCopy = new CopyOnWriteArrayList(ruleList); + + for (ContentBlocker contentBlocker : ruleListCopy) { + ContentBlockerTrigger trigger = contentBlocker.getTrigger(); + List resourceTypes = trigger.getResourceType(); + if (resourceTypes.contains(ContentBlockerTriggerResourceType.IMAGE) && !resourceTypes.contains(ContentBlockerTriggerResourceType.SVG_DOCUMENT)) { + resourceTypes.add(ContentBlockerTriggerResourceType.SVG_DOCUMENT); + } + + ContentBlockerAction action = contentBlocker.getAction(); + + Matcher m = trigger.getUrlFilterPatternCompiled().matcher(url); + if (m.matches()) { + + if (!resourceTypes.isEmpty() && !resourceTypes.contains(responseResourceType)) { + return null; + } + if (!trigger.getIfDomain().isEmpty()) { + boolean matchFound = false; + for (String domain : trigger.getIfDomain()) { + if ((domain.startsWith("*") && host.endsWith(domain.replace("*", ""))) || domain.equals(host)) { + matchFound = true; + break; + } + } + if (!matchFound) + return null; + } + if (!trigger.getUnlessDomain().isEmpty()) { + for (String domain : trigger.getUnlessDomain()) + if ((domain.startsWith("*") && host.endsWith(domain.replace("*", ""))) || domain.equals(host)) + return null; + } + + final String[] webViewUrl = new String[1]; + if (!trigger.getLoadType().isEmpty() || !trigger.getIfTopUrl().isEmpty() || !trigger.getUnlessTopUrl().isEmpty()) { + final CountDownLatch latch = new CountDownLatch(1); + Handler handler = new Handler(webView.getWebViewLooper()); + handler.post(new Runnable() { + @Override + public void run() { + webViewUrl[0] = webView.getUrl(); + latch.countDown(); + } + }); + latch.await(); + } + + if (webViewUrl[0] != null) { + if (!trigger.getLoadType().isEmpty()) { + URI cUrl = new URI(webViewUrl[0]); + String cHost = cUrl.getHost(); + int cPort = cUrl.getPort(); + String cScheme = cUrl.getScheme(); + + if ( (trigger.getLoadType().contains("first-party") && cHost != null && !(cScheme.equals(scheme) && cHost.equals(host) && cPort == port)) || + (trigger.getLoadType().contains("third-party") && cHost != null && cHost.equals(host)) ) + return null; + } + if (!trigger.getIfTopUrl().isEmpty()) { + boolean matchFound = false; + for (String topUrl : trigger.getIfTopUrl()) { + if (webViewUrl[0].startsWith(topUrl)) { + matchFound = true; + break; + } + } + if (!matchFound) + return null; + } + if (!trigger.getUnlessTopUrl().isEmpty()) { + for (String topUrl : trigger.getUnlessTopUrl()) + if (webViewUrl[0].startsWith(topUrl)) + return null; + } + } + + switch (action.getType()) { + + case BLOCK: + return new WebResourceResponse("", "", null); + + case CSS_DISPLAY_NONE: + final String cssSelector = action.getSelector(); + final String jsScript = "(function(d) { " + + " function hide () { " + + " if (d.body != null && !d.getElementById('" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "-css-display-none-style')) { " + + " var c = d.createElement('style'); " + + " c.id = '" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "-css-display-none-style'; " + + " c.innerHTML = '" + cssSelector + " { display: none !important; }'; " + + " d.body.appendChild(c); " + + " }" + + " d.querySelectorAll('" + cssSelector + "').forEach(function (item, index) { " + + " item.setAttribute('style', 'display: none !important;'); " + + " }); " + + " }; " + + " hide(); " + + " d.addEventListener('DOMContentLoaded', function(event) { hide(); }); " + + "})(document);"; + + final Handler handler = new Handler(webView.getWebViewLooper()); + handler.postDelayed(new Runnable() { + @Override + public void run() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(jsScript, null); + } else { + webView.loadUrl("javascript:" + jsScript); + } + } + }, 800); + break; + + case MAKE_HTTPS: + if (scheme.equals("http") && (port == -1 || port == 80)) { + String urlHttps = url.replace("http://", "https://"); + + HttpURLConnection urlConnection = Util.makeHttpRequest(urlHttps, request.getMethod(), request.getHeaders()); + if (urlConnection != null) { + try { + byte[] dataBytes = Util.readAllBytes(urlConnection.getInputStream()); + if (dataBytes == null) { + return null; + } + InputStream dataStream = new ByteArrayInputStream(dataBytes); + + String encoding = urlConnection.getContentEncoding(); + String contentType = urlConnection.getContentType(); + if (contentType == null) { + contentType = "text/plain"; + } else { + String[] contentTypeSplit = contentType.split(";"); + contentType = contentTypeSplit[0].trim(); + if (encoding == null) { + encoding = (contentTypeSplit.length > 1 && contentTypeSplit[1].contains("charset=")) + ? contentTypeSplit[1].replace("charset=", "").trim() + : "utf-8"; + } + } + + String reasonPhrase = urlConnection.getResponseMessage(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && reasonPhrase != null) { + Map responseHeaders = new HashMap<>(); + for (Map.Entry> responseHeader : urlConnection.getHeaderFields().entrySet()) { + responseHeaders.put(responseHeader.getKey(), TextUtils.join(",", responseHeader.getValue())); + } + return new WebResourceResponse(contentType, + encoding, + urlConnection.getResponseCode(), + reasonPhrase, + responseHeaders, + dataStream); + } else { + return new WebResourceResponse(contentType, + encoding, + dataStream); + } + } catch (Exception e) { + if (!(e instanceof SSLHandshakeException)) { + Log.e(LOG_TAG, "", e); + } + } finally { + urlConnection.disconnect(); + } + } + +// Request mRequest = new Request.Builder().url(urlHttps).build(); +// Response response = null; +// +// try { +// response = Util.getBasicOkHttpClient().newCall(mRequest).execute(); +// byte[] dataBytes = response.body().bytes(); +// InputStream dataStream = new ByteArrayInputStream(dataBytes); +// +// String[] contentTypeSplit = response.header("content-type", "text/plain").split(";"); +// +// String contentType = contentTypeSplit[0].trim(); +// String encoding = (contentTypeSplit.length > 1 && contentTypeSplit[1].contains("charset=")) +// ? contentTypeSplit[1].replace("charset=", "").trim() +// : "utf-8"; +// +// response.body().close(); +// response.close(); +// +// return new WebResourceResponse(contentType, encoding, dataStream); +// +// } catch (Exception e) { +// if (response != null) { +// response.body().close(); +// response.close(); +// } +// if (!(e instanceof SSLHandshakeException)) { +// Log.e(LOG_TAG, "", e); +// } +// } + } + break; + } + } + } + return null; + } + + @Nullable + public WebResourceResponse checkUrl(final InAppWebView webView, WebResourceRequestExt request) throws URISyntaxException, InterruptedException, MalformedURLException { + ContentBlockerTriggerResourceType responseResourceType = getResourceTypeFromUrl(request); + return checkUrl(webView, request, responseResourceType); + } + + @Nullable + public WebResourceResponse checkUrl(final InAppWebView webView, WebResourceRequestExt request, String contentType) throws URISyntaxException, InterruptedException, MalformedURLException { + ContentBlockerTriggerResourceType responseResourceType = getResourceTypeFromContentType(contentType); + return checkUrl(webView, request, responseResourceType); + } + + public ContentBlockerTriggerResourceType getResourceTypeFromUrl(WebResourceRequestExt request) { + ContentBlockerTriggerResourceType responseResourceType = ContentBlockerTriggerResourceType.RAW; + String url = request.getUrl(); + + if (url.startsWith("http://") || url.startsWith("https://")) { + // make an HTTP "HEAD" request to the server for that URL. This will not return the full content of the URL. + HttpURLConnection urlConnection = Util.makeHttpRequest(url, "HEAD", request.getHeaders()); + if (urlConnection != null) { + try { + String contentType = urlConnection.getContentType(); + if (contentType != null) { + String[] contentTypeSplit = contentType.split(";"); + contentType = contentTypeSplit[0].trim(); + responseResourceType = getResourceTypeFromContentType(contentType); + } + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } finally { + urlConnection.disconnect(); + } + } + } + return responseResourceType; + } + + public ContentBlockerTriggerResourceType getResourceTypeFromContentType(String contentType) { + ContentBlockerTriggerResourceType responseResourceType = ContentBlockerTriggerResourceType.RAW; + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types + if (contentType.equals("text/css")) { + responseResourceType = ContentBlockerTriggerResourceType.STYLE_SHEET; + } else if (contentType.equals("image/svg+xml")) { + responseResourceType = ContentBlockerTriggerResourceType.SVG_DOCUMENT; + } else if (contentType.startsWith("image/")) { + responseResourceType = ContentBlockerTriggerResourceType.IMAGE; + } else if (contentType.startsWith("font/")) { + responseResourceType = ContentBlockerTriggerResourceType.FONT; + } else if (contentType.startsWith("audio/") || contentType.startsWith("video/") || contentType.equals("application/ogg")) { + responseResourceType = ContentBlockerTriggerResourceType.MEDIA; + } else if (contentType.endsWith("javascript")) { + responseResourceType = ContentBlockerTriggerResourceType.SCRIPT; + } else if (contentType.startsWith("text/")) { + responseResourceType = ContentBlockerTriggerResourceType.DOCUMENT; + } + + return responseResourceType; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTrigger.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTrigger.java new file mode 100644 index 0000000..8efb3c0 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTrigger.java @@ -0,0 +1,185 @@ +package com.pichillilorenzo.flutter_inappwebview_android.content_blocker; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public class ContentBlockerTrigger { + + @NonNull + private String urlFilter; + private Pattern urlFilterPatternCompiled; + private Boolean urlFilterIsCaseSensitive; + private List resourceType = new ArrayList<>(); + private List ifDomain = new ArrayList<>(); + private List unlessDomain = new ArrayList<>(); + private List loadType = new ArrayList<>(); + private List ifTopUrl = new ArrayList<>(); + private List unlessTopUrl = new ArrayList<>(); + + public ContentBlockerTrigger(@NonNull String urlFilter, @Nullable Boolean urlFilterIsCaseSensitive, @Nullable List resourceType, + @Nullable List ifDomain, @Nullable List unlessDomain, @Nullable List loadType, + @Nullable List ifTopUrl, @Nullable List unlessTopUrl) { + this.urlFilterIsCaseSensitive = urlFilterIsCaseSensitive != null ? urlFilterIsCaseSensitive : false; + + this.urlFilter = urlFilter; + this.urlFilterPatternCompiled = Pattern.compile(this.urlFilter, this.urlFilterIsCaseSensitive ? 0 : Pattern.CASE_INSENSITIVE); + + this.resourceType = resourceType != null ? resourceType : this.resourceType; + this.ifDomain = ifDomain != null ? ifDomain : this.ifDomain; + this.unlessDomain = unlessDomain != null ? unlessDomain : this.unlessDomain; + if ((!(this.ifDomain.isEmpty() || this.unlessDomain.isEmpty()) != false)) + throw new AssertionError(); + this.loadType = loadType != null ? loadType : this.loadType; + if ((this.loadType.size() > 2)) throw new AssertionError(); + this.ifTopUrl = ifTopUrl != null ? ifTopUrl : this.ifTopUrl; + this.unlessTopUrl = unlessTopUrl != null ? unlessTopUrl : this.unlessTopUrl; + if ((!(this.ifTopUrl.isEmpty() || this.unlessTopUrl.isEmpty()) != false)) + throw new AssertionError(); + } + + public static ContentBlockerTrigger fromMap(Map map) { + String urlFilter = (String) map.get("url-filter"); + Boolean urlFilterIsCaseSensitive = (Boolean) map.get("url-filter-is-case-sensitive"); + List resourceTypeStringList = (List) map.get("resource-type"); + List resourceType = new ArrayList<>(); + if (resourceTypeStringList != null) { + for (String type : resourceTypeStringList) { + resourceType.add(ContentBlockerTriggerResourceType.fromValue(type)); + } + } else { + resourceType.addAll(Arrays.asList(ContentBlockerTriggerResourceType.values())); + } + List ifDomain = (List) map.get("if-domain"); + List unlessDomain = (List) map.get("unless-domain"); + List loadType = (List) map.get("load-type"); + List ifTopUrl = (List) map.get("if-top-url"); + List unlessTopUrl = (List) map.get("unless-top-url"); + return new ContentBlockerTrigger(urlFilter, urlFilterIsCaseSensitive, resourceType, ifDomain, unlessDomain, loadType, ifTopUrl, unlessTopUrl); + } + + @NonNull + public String getUrlFilter() { + return urlFilter; + } + + public void setUrlFilter(@NonNull String urlFilter) { + this.urlFilter = urlFilter; + } + + public Pattern getUrlFilterPatternCompiled() { + return urlFilterPatternCompiled; + } + + public void setUrlFilterPatternCompiled(Pattern urlFilterPatternCompiled) { + this.urlFilterPatternCompiled = urlFilterPatternCompiled; + } + + public Boolean getUrlFilterIsCaseSensitive() { + return urlFilterIsCaseSensitive; + } + + public void setUrlFilterIsCaseSensitive(Boolean urlFilterIsCaseSensitive) { + this.urlFilterIsCaseSensitive = urlFilterIsCaseSensitive; + } + + public List getResourceType() { + return resourceType; + } + + public void setResourceType(List resourceType) { + this.resourceType = resourceType; + } + + public List getIfDomain() { + return ifDomain; + } + + public void setIfDomain(List ifDomain) { + this.ifDomain = ifDomain; + } + + public List getUnlessDomain() { + return unlessDomain; + } + + public void setUnlessDomain(List unlessDomain) { + this.unlessDomain = unlessDomain; + } + + public List getLoadType() { + return loadType; + } + + public void setLoadType(List loadType) { + this.loadType = loadType; + } + + public List getIfTopUrl() { + return ifTopUrl; + } + + public void setIfTopUrl(List ifTopUrl) { + this.ifTopUrl = ifTopUrl; + } + + public List getUnlessTopUrl() { + return unlessTopUrl; + } + + public void setUnlessTopUrl(List unlessTopUrl) { + this.unlessTopUrl = unlessTopUrl; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ContentBlockerTrigger that = (ContentBlockerTrigger) o; + + if (!urlFilter.equals(that.urlFilter)) return false; + if (!urlFilterPatternCompiled.equals(that.urlFilterPatternCompiled)) return false; + if (!urlFilterIsCaseSensitive.equals(that.urlFilterIsCaseSensitive)) return false; + if (!resourceType.equals(that.resourceType)) return false; + if (!ifDomain.equals(that.ifDomain)) return false; + if (!unlessDomain.equals(that.unlessDomain)) return false; + if (!loadType.equals(that.loadType)) return false; + if (!ifTopUrl.equals(that.ifTopUrl)) return false; + return unlessTopUrl.equals(that.unlessTopUrl); + } + + @Override + public int hashCode() { + int result = urlFilter.hashCode(); + result = 31 * result + urlFilterPatternCompiled.hashCode(); + result = 31 * result + urlFilterIsCaseSensitive.hashCode(); + result = 31 * result + resourceType.hashCode(); + result = 31 * result + ifDomain.hashCode(); + result = 31 * result + unlessDomain.hashCode(); + result = 31 * result + loadType.hashCode(); + result = 31 * result + ifTopUrl.hashCode(); + result = 31 * result + unlessTopUrl.hashCode(); + return result; + } + + @Override + public String toString() { + return "ContentBlockerTrigger{" + + "urlFilter='" + urlFilter + '\'' + + ", urlFilterPatternCompiled=" + urlFilterPatternCompiled + + ", urlFilterIsCaseSensitive=" + urlFilterIsCaseSensitive + + ", resourceType=" + resourceType + + ", ifDomain=" + ifDomain + + ", unlessDomain=" + unlessDomain + + ", loadType=" + loadType + + ", ifTopUrl=" + ifTopUrl + + ", unlessTopUrl=" + unlessTopUrl + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTriggerResourceType.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTriggerResourceType.java new file mode 100644 index 0000000..25eb60b --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTriggerResourceType.java @@ -0,0 +1,36 @@ +package com.pichillilorenzo.flutter_inappwebview_android.content_blocker; + +public enum ContentBlockerTriggerResourceType { + DOCUMENT ("document"), + IMAGE ("image"), + STYLE_SHEET ("style-sheet"), + SCRIPT ("script"), + FONT ("font"), + SVG_DOCUMENT ("svg-document"), + MEDIA ("media"), + POPUP ("popup"), + RAW ("raw"); + + private final String value; + + private ContentBlockerTriggerResourceType(String value) { + this.value = value; + } + + public boolean equalsValue(String otherValue) { + return value.equals(otherValue); + } + + public static ContentBlockerTriggerResourceType fromValue(String value) { + for( ContentBlockerTriggerResourceType type : ContentBlockerTriggerResourceType.values()) { + if(value.equals(type.value)) + return type; + } + throw new IllegalArgumentException("No enum constant: " + value); + } + + @Override + public String toString() { + return this.value; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabase.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabase.java new file mode 100644 index 0000000..5d83b7e --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabase.java @@ -0,0 +1,95 @@ +package com.pichillilorenzo.flutter_inappwebview_android.credential_database; + +import android.content.Context; + +import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace; + +import java.util.ArrayList; +import java.util.List; + +public class CredentialDatabase { + + private static CredentialDatabase instance; + static final String LOG_TAG = "CredentialDatabase"; + + // If you change the database schema, you must increment the database version. + public static final int DATABASE_VERSION = 2; + public static final String DATABASE_NAME = "CredentialDatabase.db"; + + public URLProtectionSpaceDao protectionSpaceDao; + public URLCredentialDao credentialDao; + public CredentialDatabaseHelper db; + + private CredentialDatabase() {} + + private CredentialDatabase(CredentialDatabaseHelper db, URLProtectionSpaceDao protectionSpaceDao, URLCredentialDao credentialDao) { + this.db = db; + this.protectionSpaceDao = protectionSpaceDao; + this.credentialDao = credentialDao; + } + + public static CredentialDatabase getInstance(Context context) { + if (instance != null) + return instance; + CredentialDatabaseHelper db = new CredentialDatabaseHelper(context); + instance = new CredentialDatabase(db, new URLProtectionSpaceDao(db), new URLCredentialDao(db)); + return instance; + } + + public List getHttpAuthCredentials(String host, String protocol, String realm, Integer port) { + List credentials = new ArrayList<>(); + URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port); + if (protectionSpace != null) { + credentials = credentialDao.getAllByProtectionSpaceId(protectionSpace.getId()); + } + return credentials; + } + + public void clearAllAuthCredentials() { + db.clearAllTables(db.getWritableDatabase()); + } + + public void removeHttpAuthCredentials(String host, String protocol, String realm, Integer port) { + URLProtectionSpace URLProtectionSpace = protectionSpaceDao.find(host, protocol, realm, port); + if (URLProtectionSpace != null) { + protectionSpaceDao.delete(URLProtectionSpace); + } + } + + public void removeHttpAuthCredential(String host, String protocol, String realm, Integer port, String username, String password) { + URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port); + if (protectionSpace != null) { + URLCredential credential = credentialDao.find(username, password, protectionSpace.getId()); + credentialDao.delete(credential); + } + } + + public void setHttpAuthCredential(String host, String protocol, String realm, Integer port, String username, String password) { + URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port); + Long protectionSpaceId; + if (protectionSpace == null) { + protectionSpaceId = protectionSpaceDao.insert(new URLProtectionSpace(null, host, protocol, realm, port)); + } else { + protectionSpaceId = protectionSpace.getId(); + } + + URLCredential credential = credentialDao.find(username, password, protectionSpaceId); + if (credential != null) { + boolean needUpdate = false; + if (!credential.getUsername().equals(username)) { + credential.setUsername(username); + needUpdate = true; + } + if (!credential.getPassword().equals(password)) { + credential.setPassword(password); + needUpdate = true; + } + if (needUpdate) + credentialDao.update(credential); + } else { + credential = new URLCredential(null, username, password, protectionSpaceId); + credential.setId(credentialDao.insert(credential)); + } + } +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHandler.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHandler.java new file mode 100644 index 0000000..23052a1 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHandler.java @@ -0,0 +1,157 @@ +package com.pichillilorenzo.flutter_inappwebview_android.credential_database; + +import android.os.Build; +import android.webkit.WebViewDatabase; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +@RequiresApi(api = Build.VERSION_CODES.O) +public class CredentialDatabaseHandler extends ChannelDelegateImpl { + protected static final String LOG_TAG = "CredentialDatabaseHandler"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_credential_database"; + + @Nullable + public static CredentialDatabase credentialDatabase; + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public CredentialDatabaseHandler(@NonNull final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.plugin = plugin; + } + + public static void init(@NonNull InAppWebViewFlutterPlugin plugin) { + if (credentialDatabase == null) { + credentialDatabase = CredentialDatabase.getInstance(plugin.applicationContext); + } + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + if (plugin != null) { + init(plugin); + } + + switch (call.method) { + case "getAllAuthCredentials": + { + List> allCredentials = new ArrayList<>(); + if (credentialDatabase != null) { + List protectionSpaces = credentialDatabase.protectionSpaceDao.getAll(); + for (URLProtectionSpace protectionSpace : protectionSpaces) { + List> credentials = new ArrayList<>(); + for (URLCredential credential : credentialDatabase.credentialDao.getAllByProtectionSpaceId(protectionSpace.getId())) { + credentials.add(credential.toMap()); + } + Map obj = new HashMap<>(); + obj.put("protectionSpace", protectionSpace.toMap()); + obj.put("credentials", credentials); + allCredentials.add(obj); + } + } + result.success(allCredentials); + } + break; + case "getHttpAuthCredentials": + { + List> credentials = new ArrayList<>(); + if (credentialDatabase != null) { + String host = (String) call.argument("host"); + String protocol = (String) call.argument("protocol"); + String realm = (String) call.argument("realm"); + Integer port = (Integer) call.argument("port"); + + for (URLCredential credential : credentialDatabase.getHttpAuthCredentials(host, protocol, realm, port)) { + credentials.add(credential.toMap()); + } + } + result.success(credentials); + } + break; + case "setHttpAuthCredential": + { + if (credentialDatabase != null) { + String host = (String) call.argument("host"); + String protocol = (String) call.argument("protocol"); + String realm = (String) call.argument("realm"); + Integer port = (Integer) call.argument("port"); + String username = (String) call.argument("username"); + String password = (String) call.argument("password"); + + credentialDatabase.setHttpAuthCredential(host, protocol, realm, port, username, password); + result.success(true); + } else { + result.success(false); + } + } + break; + case "removeHttpAuthCredential": + { + if (credentialDatabase != null) { + String host = (String) call.argument("host"); + String protocol = (String) call.argument("protocol"); + String realm = (String) call.argument("realm"); + Integer port = (Integer) call.argument("port"); + String username = (String) call.argument("username"); + String password = (String) call.argument("password"); + + credentialDatabase.removeHttpAuthCredential(host, protocol, realm, port, username, password); + result.success(true); + } else { + result.success(false); + } + } + break; + case "removeHttpAuthCredentials": + { + if (credentialDatabase != null) { + String host = (String) call.argument("host"); + String protocol = (String) call.argument("protocol"); + String realm = (String) call.argument("realm"); + Integer port = (Integer) call.argument("port"); + + credentialDatabase.removeHttpAuthCredentials(host, protocol, realm, port); + result.success(true); + } else { + result.success(false); + } + } + break; + case "clearAllAuthCredentials": + if (credentialDatabase != null) { + credentialDatabase.clearAllAuthCredentials(); + if (plugin != null && plugin.applicationContext != null) { + WebViewDatabase.getInstance(plugin.applicationContext).clearHttpAuthUsernamePassword(); + } + result.success(true); + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + @Override + public void dispose() { + super.dispose(); + plugin = null; + credentialDatabase = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHelper.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHelper.java new file mode 100644 index 0000000..ff67e28 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHelper.java @@ -0,0 +1,66 @@ +package com.pichillilorenzo.flutter_inappwebview_android.credential_database; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class CredentialDatabaseHelper extends SQLiteOpenHelper { + + private static final String SQL_CREATE_PROTECTION_SPACE_TABLE = + "CREATE TABLE " + URLProtectionSpaceContract.FeedEntry.TABLE_NAME + " (" + + URLProtectionSpaceContract.FeedEntry._ID + " INTEGER PRIMARY KEY," + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + " TEXT NOT NULL," + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + " TEXT," + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + " TEXT," + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + " INTEGER," + + "UNIQUE(" + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + ", " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + ", " + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + ", " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + + ")" + + ");"; + + private static final String SQL_CREATE_CREDENTIAL_TABLE = + "CREATE TABLE " + URLCredentialContract.FeedEntry.TABLE_NAME + " (" + + URLCredentialContract.FeedEntry._ID + " INTEGER PRIMARY KEY," + + URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME + " TEXT NOT NULL," + + URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + " TEXT NOT NULL," + + URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " INTEGER NOT NULL," + + "UNIQUE(" + URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME + ", " + URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + ", " + + URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + + ")," + + "FOREIGN KEY (" + URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + ") REFERENCES " + + URLProtectionSpaceContract.FeedEntry.TABLE_NAME + " (" + URLProtectionSpaceContract.FeedEntry._ID + ") ON DELETE CASCADE" + + ");"; + + private static final String SQL_DELETE_PROTECTION_SPACE_TABLE = + "DROP TABLE IF EXISTS " + URLProtectionSpaceContract.FeedEntry.TABLE_NAME; + + private static final String SQL_DELETE_CREDENTIAL_TABLE = + "DROP TABLE IF EXISTS " + URLCredentialContract.FeedEntry.TABLE_NAME; + + public CredentialDatabaseHelper(Context context) { + super(context, CredentialDatabase.DATABASE_NAME, null, CredentialDatabase.DATABASE_VERSION); + } + + public void onCreate(SQLiteDatabase db) { + db.execSQL(SQL_CREATE_PROTECTION_SPACE_TABLE); + db.execSQL(SQL_CREATE_CREDENTIAL_TABLE); + } + + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // This database is only a cache for online data, so its upgrade policy is + // to simply to discard the data and start over + db.execSQL(SQL_DELETE_PROTECTION_SPACE_TABLE); + db.execSQL(SQL_DELETE_CREDENTIAL_TABLE); + onCreate(db); + } + + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + onUpgrade(db, oldVersion, newVersion); + } + + public void clearAllTables(SQLiteDatabase db) { + db.execSQL(SQL_DELETE_PROTECTION_SPACE_TABLE); + db.execSQL(SQL_DELETE_CREDENTIAL_TABLE); + onCreate(db); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialContract.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialContract.java new file mode 100644 index 0000000..d837b55 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialContract.java @@ -0,0 +1,15 @@ +package com.pichillilorenzo.flutter_inappwebview_android.credential_database; + +import android.provider.BaseColumns; + +public class URLCredentialContract { + private URLCredentialContract() {} + + /* Inner class that defines the table contents */ + public static class FeedEntry implements BaseColumns { + public static final String TABLE_NAME = "credential"; + public static final String COLUMN_NAME_USERNAME = "username"; + public static final String COLUMN_NAME_PASSWORD = "password"; + public static final String COLUMN_NAME_PROTECTION_SPACE_ID = "protection_space_id"; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialDao.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialDao.java new file mode 100644 index 0000000..2c39a41 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialDao.java @@ -0,0 +1,106 @@ +package com.pichillilorenzo.flutter_inappwebview_android.credential_database; + +import android.content.ContentValues; +import android.database.Cursor; + +import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential; + +import java.util.ArrayList; +import java.util.List; + +public class URLCredentialDao { + + CredentialDatabaseHelper credentialDatabaseHelper; + String[] projection = { + URLCredentialContract.FeedEntry._ID, + URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME, + URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, + URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + }; + + public URLCredentialDao(CredentialDatabaseHelper credentialDatabaseHelper) { + this.credentialDatabaseHelper = credentialDatabaseHelper; + } + + public List getAllByProtectionSpaceId(Long protectionSpaceId) { + String selection = URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?"; + String[] selectionArgs = {protectionSpaceId.toString()}; + + Cursor cursor = credentialDatabaseHelper.getReadableDatabase().query( + URLCredentialContract.FeedEntry.TABLE_NAME, + projection, + selection, + selectionArgs, + null, + null, + null + ); + + List URLCredentials = new ArrayList<>(); + while (cursor.moveToNext()) { + Long id = cursor.getLong(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry._ID)); + String username = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME)); + String password = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD)); + URLCredentials.add(new URLCredential(id, username, password, protectionSpaceId)); + } + cursor.close(); + + return URLCredentials; + } + + public URLCredential find(String username, String password, Long protectionSpaceId) { + String selection = URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME + " = ? AND " + + URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + " = ? AND " + + URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?"; + String[] selectionArgs = {username, password, protectionSpaceId.toString()}; + + Cursor cursor = credentialDatabaseHelper.getReadableDatabase().query( + URLCredentialContract.FeedEntry.TABLE_NAME, + projection, + selection, + selectionArgs, + null, + null, + null + ); + + URLCredential URLCredential = null; + if (cursor.moveToNext()) { + Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry._ID)); + String rowUsername = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME)); + String rowPassword = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD)); + URLCredential = new URLCredential(rowId, rowUsername, rowPassword, protectionSpaceId); + } + cursor.close(); + + return URLCredential; + } + + public long insert(URLCredential urlCredential) { + ContentValues credentialValues = new ContentValues(); + credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME, urlCredential.getUsername()); + credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, urlCredential.getPassword()); + credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID, urlCredential.getProtectionSpaceId()); + + return credentialDatabaseHelper.getWritableDatabase().insert(URLCredentialContract.FeedEntry.TABLE_NAME, null, credentialValues); + } + + public long update(URLCredential urlCredential) { + ContentValues credentialValues = new ContentValues(); + credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME, urlCredential.getUsername()); + credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, urlCredential.getPassword()); + + String whereClause = URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?"; + String[] whereArgs = {urlCredential.getProtectionSpaceId().toString()}; + + return credentialDatabaseHelper.getWritableDatabase().update(URLCredentialContract.FeedEntry.TABLE_NAME, credentialValues, whereClause, whereArgs); + } + + public long delete(URLCredential urlCredential) { + String whereClause = URLCredentialContract.FeedEntry._ID + " = ?"; + String[] whereArgs = {urlCredential.getId().toString()}; + + return credentialDatabaseHelper.getWritableDatabase().delete(URLCredentialContract.FeedEntry.TABLE_NAME, whereClause, whereArgs); + } + +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceContract.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceContract.java new file mode 100644 index 0000000..347ab5f --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceContract.java @@ -0,0 +1,16 @@ +package com.pichillilorenzo.flutter_inappwebview_android.credential_database; + +import android.provider.BaseColumns; + +public class URLProtectionSpaceContract { + private URLProtectionSpaceContract() {} + + /* Inner class that defines the table contents */ + public static class FeedEntry implements BaseColumns { + public static final String TABLE_NAME = "protection_space"; + public static final String COLUMN_NAME_HOST = "host"; + public static final String COLUMN_NAME_PROTOCOL = "protocol"; + public static final String COLUMN_NAME_REALM = "realm"; + public static final String COLUMN_NAME_PORT = "port"; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceDao.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceDao.java new file mode 100644 index 0000000..db85276 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceDao.java @@ -0,0 +1,100 @@ +package com.pichillilorenzo.flutter_inappwebview_android.credential_database; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace; + +import java.util.ArrayList; +import java.util.List; + +public class URLProtectionSpaceDao { + CredentialDatabaseHelper credentialDatabaseHelper; + String[] projection = { + URLProtectionSpaceContract.FeedEntry._ID, + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST, + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL, + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM, + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + }; + + public URLProtectionSpaceDao(CredentialDatabaseHelper credentialDatabaseHelper) { + this.credentialDatabaseHelper = credentialDatabaseHelper; + } + + public List getAll() { + SQLiteDatabase readableDatabase = credentialDatabaseHelper.getReadableDatabase(); + + Cursor cursor = readableDatabase.query( + URLProtectionSpaceContract.FeedEntry.TABLE_NAME, + projection, + null, + null, + null, + null, + null + ); + + List URLProtectionSpaces = new ArrayList<>(); + while (cursor.moveToNext()) { + Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry._ID)); + String rowHost = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST)); + String rowProtocol = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL)); + String rowRealm = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM)); + Integer rowPort = cursor.getInt(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT)); + URLProtectionSpaces.add(new URLProtectionSpace(rowId, rowHost, rowProtocol, rowRealm, rowPort)); + } + cursor.close(); + + return URLProtectionSpaces; + } + + public URLProtectionSpace find(String host, String protocol, String realm, Integer port) { + SQLiteDatabase readableDatabase = credentialDatabaseHelper.getReadableDatabase(); + + String selection = URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + " = ? AND " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + " = ? AND " + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + " = ? AND " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + " = ?"; + String[] selectionArgs = {host, protocol, realm, port.toString()}; + + Cursor cursor = readableDatabase.query( + URLProtectionSpaceContract.FeedEntry.TABLE_NAME, + projection, + selection, + selectionArgs, + null, + null, + null + ); + + URLProtectionSpace URLProtectionSpace = null; + if (cursor.moveToNext()) { + Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry._ID)); + String rowHost = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST)); + String rowProtocol = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL)); + String rowRealm = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM)); + Integer rowPort = cursor.getInt(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT)); + URLProtectionSpace = new URLProtectionSpace(rowId, rowHost, rowProtocol, rowRealm, rowPort); + } + cursor.close(); + + return URLProtectionSpace; + } + + public long insert(URLProtectionSpace URLProtectionSpace) { + ContentValues protectionSpaceValues = new ContentValues(); + protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST, URLProtectionSpace.getHost()); + protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL, URLProtectionSpace.getProtocol()); + protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM, URLProtectionSpace.getRealm()); + protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT, URLProtectionSpace.getPort()); + + return credentialDatabaseHelper.getWritableDatabase().insert(URLProtectionSpaceContract.FeedEntry.TABLE_NAME, null, protectionSpaceValues); + }; + + public long delete(URLProtectionSpace URLProtectionSpace) { + String whereClause = URLProtectionSpaceContract.FeedEntry._ID + " = ?"; + String[] whereArgs = {URLProtectionSpace.getId().toString()}; + + return credentialDatabaseHelper.getWritableDatabase().delete(URLProtectionSpaceContract.FeedEntry.TABLE_NAME, whereClause, whereArgs); + } +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionChannelDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionChannelDelegate.java new file mode 100644 index 0000000..a0be164 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionChannelDelegate.java @@ -0,0 +1,94 @@ +package com.pichillilorenzo.flutter_inappwebview_android.find_interaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.FindSession; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class FindInteractionChannelDelegate extends ChannelDelegateImpl { + @Nullable + private FindInteractionController findInteractionController; + + public FindInteractionChannelDelegate(@NonNull FindInteractionController findInteractionController, @NonNull MethodChannel channel) { + super(channel); + this.findInteractionController = findInteractionController; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) { + switch (call.method) { + case "findAll": + if (findInteractionController != null) { + String find = (String) call.argument("find"); + findInteractionController.findAll(find); + } + result.success(true); + break; + case "findNext": + if (findInteractionController != null) { + Boolean forward = (Boolean) call.argument("forward"); + findInteractionController.findNext(forward); + } + result.success(true); + break; + case "clearMatches": + if (findInteractionController != null) { + findInteractionController.clearMatches(); + } + result.success(true); + break; + case "setSearchText": + if (findInteractionController != null) { + findInteractionController.searchText = (String) call.argument("searchText"); + result.success(true); + } else { + result.success(false); + } + break; + case "getSearchText": + if (findInteractionController != null) { + result.success(findInteractionController.searchText); + } else { + result.success(false); + } + break; + case "getActiveFindSession": + if (findInteractionController != null && findInteractionController.activeFindSession != null) { + result.success(findInteractionController.activeFindSession.toMap()); + } else { + result.success(null); + } + break; + default: + result.notImplemented(); + } + } + + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { + MethodChannel channel = getChannel(); + if (channel == null) return; + + if (isDoneCounting && findInteractionController != null && findInteractionController.webView != null) { + findInteractionController.activeFindSession = new FindSession(numberOfMatches, activeMatchOrdinal); + } + + Map obj = new HashMap<>(); + obj.put("activeMatchOrdinal", activeMatchOrdinal); + obj.put("numberOfMatches", numberOfMatches); + obj.put("isDoneCounting", isDoneCounting); + channel.invokeMethod("onFindResultReceived", obj); + } + + @Override + public void dispose() { + super.dispose(); + findInteractionController = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionController.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionController.java new file mode 100644 index 0000000..64f9342 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionController.java @@ -0,0 +1,73 @@ +package com.pichillilorenzo.flutter_inappwebview_android.find_interaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview_android.types.FindSession; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface; + +import io.flutter.plugin.common.MethodChannel; + +public class FindInteractionController implements Disposable { + static final String LOG_TAG = "FindInteractionController"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_find_interaction_"; + + @Nullable + public InAppWebViewInterface webView; + @Nullable + public FindSession activeFindSession; + @Nullable + public FindInteractionChannelDelegate channelDelegate; + @Nullable + public FindInteractionSettings settings; + @Nullable + public String searchText; + + public FindInteractionController(@NonNull InAppWebViewInterface webView, @NonNull InAppWebViewFlutterPlugin plugin, + @NonNull Object id, @Nullable FindInteractionSettings settings) { + this.webView = webView; + this.settings = settings; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id); + this.channelDelegate = new FindInteractionChannelDelegate(this, channel); + } + + public void prepare() { + + } + + public void findAll(@Nullable String find) { + if (find == null) { + find = searchText; + } else { + // updated searchText + searchText = find; + } + if (webView != null && find != null) { + webView.findAllAsync(find); + } + } + + public void findNext(boolean forward) { + if (webView != null) { + webView.findNext(forward); + } + } + + public void clearMatches() { + if (webView != null) { + webView.clearMatches(); + } + } + + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + webView = null; + activeFindSession = null; + searchText = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionSettings.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionSettings.java new file mode 100644 index 0000000..1b4d0c6 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionSettings.java @@ -0,0 +1,45 @@ +package com.pichillilorenzo.flutter_inappwebview_android.find_interaction; + +import androidx.annotation.NonNull; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; + +import java.util.HashMap; +import java.util.Map; + +public class FindInteractionSettings implements ISettings { + public static final String LOG_TAG = "FindInteractionSettings"; + + + @NonNull + @Override + public FindInteractionSettings parse(@NonNull Map settings) { +// for (Map.Entry pair : settings.entrySet()) { +// String key = pair.getKey(); +// Object value = pair.getValue(); +// if (value == null) { +// continue; +// } +// +// switch (key) { +// +// } +// } + + return this; + } + + @NonNull + public Map toMap() { + Map settings = new HashMap<>(); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull FindInteractionController findInteractionController) { + Map realSettings = toMap(); + return realSettings; + } + +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebView.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebView.java new file mode 100644 index 0000000..129c192 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebView.java @@ -0,0 +1,159 @@ +package com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview; + +import android.app.Activity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview_android.types.Size2D; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.FlutterWebView; + +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +public class HeadlessInAppWebView implements Disposable { + protected static final String LOG_TAG = "HeadlessInAppWebView"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_headless_inappwebview_"; + + @NonNull + public final String id; + @Nullable + public HeadlessWebViewChannelDelegate channelDelegate; + @Nullable + public FlutterWebView flutterWebView; + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public HeadlessInAppWebView(@NonNull final InAppWebViewFlutterPlugin plugin, @NonNull String id, @NonNull FlutterWebView flutterWebView) { + this.id = id; + this.plugin = plugin; + this.flutterWebView = flutterWebView; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id); + this.channelDelegate = new HeadlessWebViewChannelDelegate(this, channel); + } + + public void onWebViewCreated() { + if (channelDelegate != null) { + channelDelegate.onWebViewCreated(); + } + } + + public void prepare(Map params) { + if (flutterWebView != null) { + View view = flutterWebView.getView(); + if (view != null) { + final Map initialSize = (Map) params.get("initialSize"); + Size2D size = Size2D.fromMap(initialSize); + if (size == null) { + size = new Size2D(-1, -1); + } + setSize(size); + view.setVisibility(View.INVISIBLE); + } + } + if (plugin != null && plugin.activity != null) { + // Add the headless WebView to the view hierarchy. + // This way is also possible to take screenshots. + ViewGroup contentView = (ViewGroup) plugin.activity.findViewById(android.R.id.content); + if (contentView != null) { + ViewGroup mainView = (ViewGroup) (contentView).getChildAt(0); + if (mainView != null && flutterWebView != null) { + View view = flutterWebView.getView(); + if (view != null) { + mainView.addView(view, 0); + } + } + } + } + } + + public void setSize(@NonNull Size2D size) { + if (flutterWebView != null && flutterWebView.webView != null) { + View view = flutterWebView.getView(); + if (view != null) { + float scale = Util.getPixelDensity(view.getContext()); + Size2D fullscreenSize = Util.getFullscreenSize(view.getContext()); + int width = (int) (size.getWidth() == -1 ? fullscreenSize.getWidth() : (size.getWidth() * scale)); + int height = (int) (size.getWidth() == -1 ? fullscreenSize.getHeight() : (size.getHeight() * scale)); + view.setLayoutParams(new FrameLayout.LayoutParams(width, height)); + } + } + } + + @Nullable + public Size2D getSize() { + if (flutterWebView != null && flutterWebView.webView != null) { + View view = flutterWebView.getView(); + if (view != null) { + float scale = Util.getPixelDensity(view.getContext()); + Size2D fullscreenSize = Util.getFullscreenSize(view.getContext()); + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + return new Size2D( + fullscreenSize.getWidth() == layoutParams.width ? layoutParams.width : (layoutParams.width / scale), + fullscreenSize.getHeight() == layoutParams.height ? layoutParams.height : (layoutParams.height / scale) + ); + } + } + return null; + } + + @Nullable + public FlutterWebView disposeAndGetFlutterWebView() { + FlutterWebView newFlutterWebView = flutterWebView; + if (flutterWebView != null) { + View view = flutterWebView.getView(); + if (view != null) { + // restore WebView layout params and visibility + view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + view.setVisibility(View.VISIBLE); + // remove from parent + ViewGroup parent = (ViewGroup) view.getParent(); + if (parent != null) { + parent.removeView(view); + } + } + // set to null to avoid to be disposed before calling "dispose()" + flutterWebView = null; + dispose(); + } + return newFlutterWebView; + } + + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + if (plugin != null) { + HeadlessInAppWebViewManager headlessInAppWebViewManager = plugin.headlessInAppWebViewManager; + if (headlessInAppWebViewManager != null && headlessInAppWebViewManager.webViews.containsKey(id)) { + headlessInAppWebViewManager.webViews.put(id, null); + } + Activity activity = plugin.activity; + if (activity != null) { + ViewGroup contentView = plugin.activity.findViewById(android.R.id.content); + if (contentView != null) { + ViewGroup mainView = (ViewGroup) (contentView).getChildAt(0); + if (mainView != null && flutterWebView != null) { + View view = flutterWebView.getView(); + if (view != null) { + mainView.removeView(flutterWebView.getView()); + } + } + } + } + } + if (flutterWebView != null) { + flutterWebView.dispose(); + } + flutterWebView = null; + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebViewManager.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebViewManager.java new file mode 100644 index 0000000..619e174 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebViewManager.java @@ -0,0 +1,98 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +package com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview; + +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.annotation.NonNull; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.FlutterWebView; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.Result; + +public class HeadlessInAppWebViewManager extends ChannelDelegateImpl { + protected static final String LOG_TAG = "HeadlessInAppWebViewManager"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_headless_inappwebview"; + + public final Map webViews = new HashMap<>(); + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public HeadlessInAppWebViewManager(final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.plugin = plugin; + } + + @Override + public void onMethodCall(final MethodCall call, @NonNull final Result result) { + final String id = (String) call.argument("id"); + + switch (call.method) { + case "run": + { + HashMap params = (HashMap) call.argument("params"); + run(id, params); + } + result.success(true); + break; + default: + result.notImplemented(); + } + } + + public void run(String id, HashMap params) { + if (plugin == null || (plugin.activity == null && plugin.applicationContext == null)) return; + Context context = plugin.activity; + if (context == null) { + context = plugin.applicationContext; + } + FlutterWebView flutterWebView = new FlutterWebView(plugin, context, id, params); + HeadlessInAppWebView headlessInAppWebView = new HeadlessInAppWebView(plugin, id, flutterWebView); + webViews.put(id, headlessInAppWebView); + + headlessInAppWebView.prepare(params); + headlessInAppWebView.onWebViewCreated(); + flutterWebView.makeInitialLoad(params); + } + + @Override + public void dispose() { + super.dispose(); + Collection headlessInAppWebViews = webViews.values(); + for (HeadlessInAppWebView headlessInAppWebView : headlessInAppWebViews) { + if (headlessInAppWebView != null) { + headlessInAppWebView.dispose(); + } + } + webViews.clear(); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessWebViewChannelDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessWebViewChannelDelegate.java new file mode 100644 index 0000000..96d922c --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessWebViewChannelDelegate.java @@ -0,0 +1,71 @@ +package com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.Size2D; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class HeadlessWebViewChannelDelegate extends ChannelDelegateImpl { + @Nullable + private HeadlessInAppWebView headlessWebView; + + public HeadlessWebViewChannelDelegate(@NonNull HeadlessInAppWebView headlessWebView, @NonNull MethodChannel channel) { + super(channel); + this.headlessWebView = headlessWebView; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + switch (call.method) { + case "dispose": + if (headlessWebView != null) { + headlessWebView.dispose(); + result.success(true); + } else { + result.success(false); + } + break; + case "setSize": + if (headlessWebView != null) { + Map sizeMap = (Map) call.argument("size"); + Size2D size = Size2D.fromMap(sizeMap); + if (size != null) + headlessWebView.setSize(size); + result.success(true); + } else { + result.success(false); + } + break; + case "getSize": + if (headlessWebView != null) { + Size2D size = headlessWebView.getSize(); + result.success(size != null ? size.toMap() : null); + } else { + result.success(null); + } + break; + default: + result.notImplemented(); + } + } + + public void onWebViewCreated() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onWebViewCreated", obj); + } + + @Override + public void dispose() { + super.dispose(); + headlessWebView = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/ActivityResultListener.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/ActivityResultListener.java new file mode 100644 index 0000000..a50f333 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/ActivityResultListener.java @@ -0,0 +1,8 @@ +package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser; + +import android.content.Intent; + +public interface ActivityResultListener { + /** @return true if the result has been handled. */ + boolean onActivityResult(int requestCode, int resultCode, Intent data); +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserActivity.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserActivity.java new file mode 100644 index 0000000..6cb763c --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserActivity.java @@ -0,0 +1,643 @@ +package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Message; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.webkit.WebView; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.SearchView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.view.menu.MenuBuilder; + +import com.pichillilorenzo.flutter_inappwebview_android.R; +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.find_interaction.FindInteractionController; +import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshChannelDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshLayout; +import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshSettings; +import com.pichillilorenzo.flutter_inappwebview_android.types.AndroidResource; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview_android.types.InAppBrowserMenuItem; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScript; +import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebViewSettings; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +public class InAppBrowserActivity extends AppCompatActivity implements InAppBrowserDelegate, Disposable { + protected static final String LOG_TAG = "InAppBrowserActivity"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappbrowser_"; + + @Nullable + public Integer windowId; + public String id; + @Nullable + public InAppWebView webView; + @Nullable + public PullToRefreshLayout pullToRefreshLayout; + @Nullable + public ActionBar actionBar; + @Nullable + public Menu menu; + @Nullable + public SearchView searchView; + public InAppBrowserSettings customSettings = new InAppBrowserSettings(); + @Nullable + public ProgressBar progressBar; + public boolean isHidden = false; + @Nullable + public String fromActivity; + private List activityResultListeners = new ArrayList<>(); + @Nullable + public InAppBrowserManager manager; + @Nullable + public InAppBrowserChannelDelegate channelDelegate; + public List menuItems = new ArrayList<>(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle b = getIntent().getExtras(); + if (b == null) return; + + id = b.getString("id"); + + String managerId = b.getString("managerId"); + manager = InAppBrowserManager.shared.get(managerId); + if (manager == null || manager.plugin == null|| manager.plugin.messenger == null) return; + + Map settingsMap = (Map) b.getSerializable("settings"); + customSettings.parse(settingsMap); + + windowId = b.getInt("windowId"); + + setContentView(R.layout.activity_web_view); + + Map pullToRefreshInitialSettings = (Map) b.getSerializable("pullToRefreshInitialSettings"); + MethodChannel pullToRefreshLayoutChannel = new MethodChannel(manager.plugin.messenger, PullToRefreshLayout.METHOD_CHANNEL_NAME_PREFIX + id); + PullToRefreshSettings pullToRefreshSettings = new PullToRefreshSettings(); + pullToRefreshSettings.parse(pullToRefreshInitialSettings); + pullToRefreshLayout = findViewById(R.id.pullToRefresh); + pullToRefreshLayout.channelDelegate = new PullToRefreshChannelDelegate(pullToRefreshLayout, pullToRefreshLayoutChannel); + pullToRefreshLayout.settings = pullToRefreshSettings; + pullToRefreshLayout.prepare(); + + webView = findViewById(R.id.webView); + webView.id = id; + webView.windowId = windowId; + webView.inAppBrowserDelegate = this; + webView.plugin = manager.plugin; + + FindInteractionController findInteractionController = new FindInteractionController(webView, manager.plugin, id, null); + webView.findInteractionController = findInteractionController; + findInteractionController.prepare(); + + final MethodChannel channel = new MethodChannel(manager.plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id); + channelDelegate = new InAppBrowserChannelDelegate(channel); + webView.channelDelegate = new WebViewChannelDelegate(webView, channel); + + fromActivity = b.getString("fromActivity"); + + Map contextMenu = (Map) b.getSerializable("contextMenu"); + List> initialUserScripts = (List>) b.getSerializable("initialUserScripts"); + List> menuItemList = (List>) b.getSerializable("menuItems"); + for (Map menuItem : menuItemList) { + menuItems.add(InAppBrowserMenuItem.fromMap(menuItem)); + } + + InAppWebViewSettings webViewSettings = new InAppWebViewSettings(); + webViewSettings.parse(settingsMap); + webView.customSettings = webViewSettings; + webView.contextMenu = contextMenu; + + List userScripts = new ArrayList<>(); + if (initialUserScripts != null) { + for (Map initialUserScript : initialUserScripts) { + userScripts.add(UserScript.fromMap(initialUserScript)); + } + } + webView.userContentController.addUserOnlyScripts(userScripts); + + actionBar = getSupportActionBar(); + + prepareView(); + + if (windowId != -1) { + if (webView.plugin != null && webView.plugin.inAppWebViewManager != null) { + Message resultMsg = webView.plugin.inAppWebViewManager.windowWebViewMessages.get(windowId); + if (resultMsg != null) { + ((WebView.WebViewTransport) resultMsg.obj).setWebView(webView); + resultMsg.sendToTarget(); + } + } + } else { + String initialFile = b.getString("initialFile"); + Map initialUrlRequest = (Map) b.getSerializable("initialUrlRequest"); + String initialData = b.getString("initialData"); + if (initialFile != null) { + try { + webView.loadFile(initialFile); + } catch (IOException e) { + Log.e(LOG_TAG, initialFile + " asset file cannot be found!", e); + return; + } + } + else if (initialData != null) { + String mimeType = b.getString("initialMimeType"); + String encoding = b.getString("initialEncoding"); + String baseUrl = b.getString("initialBaseUrl"); + String historyUrl = b.getString("initialHistoryUrl"); + webView.loadDataWithBaseURL(baseUrl, initialData, mimeType, encoding, historyUrl); + } + else if (initialUrlRequest != null) { + URLRequest urlRequest = URLRequest.fromMap(initialUrlRequest); + if (urlRequest != null) { + webView.loadUrl(urlRequest); + } + } + } + + if (channelDelegate != null) { + channelDelegate.onBrowserCreated(); + } + } + + private void prepareView() { + + if (webView != null) { + webView.prepare(); + } + + if (customSettings.hidden) + hide(); + else + show(); + + progressBar = findViewById(R.id.progressBar); + + if (progressBar != null) { + if (customSettings.hideProgressBar) + progressBar.setMax(0); + else + progressBar.setMax(100); + } + + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(!customSettings.hideTitleBar); + + if (customSettings.hideToolbarTop) + actionBar.hide(); + + if (customSettings.toolbarTopBackgroundColor != null && !customSettings.toolbarTopBackgroundColor.isEmpty()) + actionBar.setBackgroundDrawable(new ColorDrawable(Color.parseColor(customSettings.toolbarTopBackgroundColor))); + + if (customSettings.toolbarTopFixedTitle != null && !customSettings.toolbarTopFixedTitle.isEmpty()) + actionBar.setTitle(customSettings.toolbarTopFixedTitle); + } + } + + @SuppressLint("RestrictedApi") + @Override + public boolean onCreateOptionsMenu(Menu m) { + menu = m; + + if (actionBar != null && (customSettings.toolbarTopFixedTitle == null || customSettings.toolbarTopFixedTitle.isEmpty())) + actionBar.setTitle(webView != null ? webView.getTitle() : ""); + + if (menu == null) + return super.onCreateOptionsMenu(m); + + if (menu instanceof MenuBuilder) { + ((MenuBuilder) menu).setOptionalIconsVisible(true); + } + + MenuInflater inflater = getMenuInflater(); + // Inflate menu to add items to action bar if it is present. + inflater.inflate(R.menu.menu_main, menu); + + MenuItem menuSearchItem = menu.findItem(R.id.menu_search); + if (menuSearchItem != null) { + if (customSettings.hideUrlBar) + menuSearchItem.setVisible(false); + + searchView = (SearchView) menuSearchItem.getActionView(); + if (searchView != null) { + searchView.setFocusable(true); + + searchView.setQuery(webView != null ? webView.getUrl() : "", false); + + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + if (!query.isEmpty()) { + if (webView != null) + webView.loadUrl(query); + if (searchView != null) { + searchView.setQuery("", false); + searchView.setIconified(true); + } + return true; + } + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + return false; + } + + }); + + searchView.setOnCloseListener(new SearchView.OnCloseListener() { + @Override + public boolean onClose() { + if (searchView != null && searchView.getQuery().toString().isEmpty()) + searchView.setQuery(webView != null ? webView.getUrl() : "", false); + return false; + } + }); + + searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean b) { + if (!b && searchView != null) { + searchView.setQuery("", false); + searchView.setIconified(true); + } + } + }); + } + } + + if (customSettings.hideDefaultMenuItems) { + MenuItem actionClose = menu.findItem(R.id.action_close); + if (actionClose != null) { + actionClose.setVisible(false); + } + MenuItem actionGoBack = menu.findItem(R.id.action_go_back); + if (actionGoBack != null) { + actionGoBack.setVisible(false); + } + MenuItem actionReload = menu.findItem(R.id.action_reload); + if (actionReload != null) { + actionReload.setVisible(false); + } + MenuItem actionGoForward = menu.findItem(R.id.action_go_forward); + if (actionGoForward != null) { + actionGoForward.setVisible(false); + } + MenuItem actionShare = menu.findItem(R.id.action_share); + if (actionShare != null) { + actionShare.setVisible(false); + } + } + + for (final InAppBrowserMenuItem menuItem : menuItems) { + int order = menuItem.getOrder() != null ? menuItem.getOrder() : Menu.NONE; + MenuItem item = menu.add(Menu.NONE, menuItem.getId(), order, menuItem.getTitle()); + if (menuItem.isShowAsAction()) { + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + } + Object icon = menuItem.getIcon(); + if (icon != null) { + if (icon instanceof AndroidResource) { + item.setIcon(((AndroidResource) icon).getIdentifier(this)); + } else { + item.setIcon(Util.drawableFromBytes(this, (byte[]) icon)); + } + String iconColor = menuItem.getIconColor(); + if (iconColor != null && !iconColor.isEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + item.getIcon().setTint(Color.parseColor(iconColor)); + } + } + item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(@NonNull MenuItem item) { + if (channelDelegate != null) { + channelDelegate.onMenuItemClicked(menuItem); + } + return true; + } + }); + } + + return true; + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (customSettings.shouldCloseOnBackButtonPressed) { + close(null); + return true; + } + if (customSettings.allowGoBackWithBackButton) { + if (canGoBack()) + goBack(); + else if (customSettings.closeOnCannotGoBack) + close(null); + return true; + } + if (!customSettings.shouldCloseOnBackButtonPressed) { + return true; + } + } + return super.onKeyDown(keyCode, event); + } + + public void close(final MethodChannel.Result result) { + if (channelDelegate != null) { + channelDelegate.onExit(); + } + + dispose(); + + if (result != null) { + result.success(true); + } + } + + public void reload() { + if (webView != null) + webView.reload(); + } + + public void goBack() { + if (webView != null && canGoBack()) + webView.goBack(); + } + + public boolean canGoBack() { + if (webView != null) + return webView.canGoBack(); + return false; + } + + public void goForward() { + if (webView != null && canGoForward()) + webView.goForward(); + } + + public boolean canGoForward() { + if (webView != null) + return webView.canGoForward(); + return false; + } + + public void hide() { + if (fromActivity != null) { + try { + isHidden = true; + Intent openActivity = new Intent(this, Class.forName(fromActivity)); + openActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + startActivityIfNeeded(openActivity, 0); + } catch (ClassNotFoundException e) { + Log.d(LOG_TAG, "", e); + } + } + } + + public void show() { + isHidden = false; + Intent openActivity = new Intent(this, InAppBrowserActivity.class); + openActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + startActivityIfNeeded(openActivity, 0); + } + + public void goBackButtonClicked(MenuItem item) { + goBack(); + } + + public void goForwardButtonClicked(MenuItem item) { + goForward(); + } + + public void shareButtonClicked(MenuItem item) { + Intent share = new Intent(Intent.ACTION_SEND); + share.setType("text/plain"); + share.putExtra(Intent.EXTRA_TEXT, webView != null ? webView.getUrl() : ""); + startActivity(Intent.createChooser(share, "Share")); + } + + public void reloadButtonClicked(MenuItem item) { + reload(); + } + + public void closeButtonClicked(MenuItem item) { + close(null); + } + + public void setSettings(InAppBrowserSettings newSettings, HashMap newSettingsMap) { + + InAppWebViewSettings newInAppWebViewSettings = new InAppWebViewSettings(); + newInAppWebViewSettings.parse(newSettingsMap); + if (webView != null) { + webView.setSettings(newInAppWebViewSettings, newSettingsMap); + } + + if (newSettingsMap.get("hidden") != null && customSettings.hidden != newSettings.hidden) { + if (newSettings.hidden) + hide(); + else + show(); + } + + if (newSettingsMap.get("hideProgressBar") != null && customSettings.hideProgressBar != newSettings.hideProgressBar && progressBar != null) { + if (newSettings.hideProgressBar) + progressBar.setMax(0); + else + progressBar.setMax(100); + } + + if (actionBar != null && newSettingsMap.get("hideTitleBar") != null && customSettings.hideTitleBar != newSettings.hideTitleBar) + actionBar.setDisplayShowTitleEnabled(!newSettings.hideTitleBar); + + if (actionBar != null && newSettingsMap.get("hideToolbarTop") != null && customSettings.hideToolbarTop != newSettings.hideToolbarTop) { + if (newSettings.hideToolbarTop) + actionBar.hide(); + else + actionBar.show(); + } + + if (actionBar != null && newSettingsMap.get("toolbarTopBackgroundColor") != null && + !Util.objEquals(customSettings.toolbarTopBackgroundColor, newSettings.toolbarTopBackgroundColor) && + newSettings.toolbarTopBackgroundColor != null && !newSettings.toolbarTopBackgroundColor.isEmpty()) + actionBar.setBackgroundDrawable(new ColorDrawable(Color.parseColor(newSettings.toolbarTopBackgroundColor))); + + if (actionBar != null && newSettingsMap.get("toolbarTopFixedTitle") != null && + !Util.objEquals(customSettings.toolbarTopFixedTitle, newSettings.toolbarTopFixedTitle) && + newSettings.toolbarTopFixedTitle != null && !newSettings.toolbarTopFixedTitle.isEmpty()) + actionBar.setTitle(newSettings.toolbarTopFixedTitle); + + if (menu != null && newSettingsMap.get("hideUrlBar") != null && customSettings.hideUrlBar != newSettings.hideUrlBar) { + MenuItem menuSearchItem = menu.findItem(R.id.menu_search); + if (menuSearchItem != null) { + menuSearchItem.setVisible(!newSettings.hideUrlBar); + } + } + + if (menu != null && newSettingsMap.get("hideDefaultMenuItems") != null && customSettings.hideDefaultMenuItems != newSettings.hideDefaultMenuItems) { + MenuItem actionClose = menu.findItem(R.id.action_close); + if (actionClose != null) { + actionClose.setVisible(!newSettings.hideDefaultMenuItems); + } + MenuItem actionGoBack = menu.findItem(R.id.action_go_back); + if (actionGoBack != null) { + actionGoBack.setVisible(!newSettings.hideDefaultMenuItems); + } + MenuItem actionReload = menu.findItem(R.id.action_reload); + if (actionReload != null) { + actionReload.setVisible(!newSettings.hideDefaultMenuItems); + } + MenuItem actionGoForward = menu.findItem(R.id.action_go_forward); + if (actionGoForward != null) { + actionGoForward.setVisible(!newSettings.hideDefaultMenuItems); + } + MenuItem actionShare = menu.findItem(R.id.action_share); + if (actionShare != null) { + actionShare.setVisible(!newSettings.hideDefaultMenuItems); + } + } + + customSettings = newSettings; + } + + public Map getCustomSettings() { + Map webViewSettingsMap = webView != null ? webView.getCustomSettings() : null; + if (customSettings == null || webViewSettingsMap == null) + return null; + + Map settingsMap = customSettings.getRealSettings(this); + settingsMap.putAll(webViewSettingsMap); + return settingsMap; + } + + @Override + public Activity getActivity() { + return this; + } + + @Override + public void didChangeTitle(String title) { + if (actionBar != null && (customSettings.toolbarTopFixedTitle == null || customSettings.toolbarTopFixedTitle.isEmpty())) { + actionBar.setTitle(title); + } + } + + @Override + public void didStartNavigation(String url) { + if (progressBar != null) { + progressBar.setProgress(0); + } + if (searchView != null) { + searchView.setQuery(url, false); + } + } + + @Override + public void didUpdateVisitedHistory(String url) { + if (searchView != null) { + searchView.setQuery(url, false); + } + } + + @Override + public void didFinishNavigation(String url) { + if (searchView != null) { + searchView.setQuery(url, false); + } + if (progressBar != null) { + progressBar.setProgress(0); + } + } + + @Override + public void didFailNavigation(String url, int errorCode, String description) { + if (progressBar != null) { + progressBar.setProgress(0); + } + } + + @Override + public void didChangeProgress(int progress) { + if (progressBar != null) { + progressBar.setVisibility(View.VISIBLE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + progressBar.setProgress(progress, true); + } else { + progressBar.setProgress(progress); + } + if (progress == 100) { + progressBar.setVisibility(View.GONE); + } + } + } + + public List getActivityResultListeners() { + return activityResultListeners; + } + + @Override + protected void onActivityResult (int requestCode, + int resultCode, + Intent data) { + for (ActivityResultListener listener : activityResultListeners) { + if (listener.onActivityResult(requestCode, resultCode, data)) { + return; + } + } + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + activityResultListeners.clear(); + if (webView != null) { + if (manager != null && manager.plugin != null && + manager.plugin.activityPluginBinding != null && webView.inAppWebViewChromeClient != null) { + manager.plugin.activityPluginBinding.removeActivityResultListener(webView.inAppWebViewChromeClient); + } + RelativeLayout containerView = (RelativeLayout) findViewById(R.id.container); + if (containerView != null) { + containerView.removeAllViews(); + } + webView.dispose(); + webView = null; + finish(); + } + } + + @Override + public void onDestroy() { + dispose(); + super.onDestroy(); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserChannelDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserChannelDelegate.java new file mode 100644 index 0000000..7f4ec37 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserChannelDelegate.java @@ -0,0 +1,39 @@ +package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser; + +import androidx.annotation.NonNull; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.InAppBrowserMenuItem; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +public class InAppBrowserChannelDelegate extends ChannelDelegateImpl { + public InAppBrowserChannelDelegate(@NonNull MethodChannel channel) { + super(channel); + } + + public void onBrowserCreated() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onBrowserCreated", obj); + } + + public void onMenuItemClicked(InAppBrowserMenuItem menuItem) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("id", menuItem.getId()); + channel.invokeMethod("onMenuItemClicked", obj); + } + + public void onExit() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onExit", obj); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserDelegate.java new file mode 100644 index 0000000..514866f --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserDelegate.java @@ -0,0 +1,16 @@ +package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser; + +import android.app.Activity; + +import java.util.List; + +public interface InAppBrowserDelegate { + Activity getActivity(); + List getActivityResultListeners(); + void didChangeTitle(String title); + void didStartNavigation(String url); + void didUpdateVisitedHistory(String url); + void didFinishNavigation(String url); + void didFailNavigation(String url, int errorCode, String description); + void didChangeProgress(int progress); +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserManager.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserManager.java new file mode 100644 index 0000000..b2d648e --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserManager.java @@ -0,0 +1,219 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcelable; +import android.provider.Browser; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.Result; + +/** + * InAppBrowserManager + */ +public class InAppBrowserManager extends ChannelDelegateImpl { + protected static final String LOG_TAG = "InAppBrowserManager"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappbrowser"; + + @Nullable + public InAppWebViewFlutterPlugin plugin; + public String id; + public static final Map shared = new HashMap<>(); + + public InAppBrowserManager(final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.id = UUID.randomUUID().toString(); + this.plugin = plugin; + shared.put(this.id, this); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + switch (call.method) { + case "open": + if (plugin != null && plugin.activity != null) { + open(plugin.activity, (Map) call.arguments()); + result.success(true); + } else { + result.success(false); + } + break; + case "openWithSystemBrowser": + if (plugin != null && plugin.activity != null) { + String url = (String) call.argument("url"); + openWithSystemBrowser(plugin.activity, url, result); + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + public static String getMimeType(String url) { + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + return type; + } + + /** + * Display a new browser with the specified URL. + * + * @param url the url to load. + * @param result + * @return "" if ok, or error message. + */ + public void openWithSystemBrowser(Activity activity, String url, Result result) { + try { + Intent intent; + intent = new Intent(Intent.ACTION_VIEW); + // Omitting the MIME type for file: URLs causes "No Activity found to handle Intent". + // Adding the MIME type to http: URLs causes them to not be handled by the downloader. + Uri uri = Uri.parse(url); + if ("file".equals(uri.getScheme())) { + intent.setDataAndType(uri, getMimeType(url)); + } else { + intent.setData(uri); + } + intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName()); + // CB-10795: Avoid circular loops by preventing it from opening in the current app + this.openExternalExcludeCurrentApp(activity, intent); + result.success(true); + // not catching FileUriExposedException explicitly because buildtools<24 doesn't know about it + } catch (java.lang.RuntimeException e) { + Log.d(LOG_TAG, url + " cannot be opened: " + e.toString()); + result.error(LOG_TAG, url + " cannot be opened!", null); + } + } + + /** + * Opens the intent, providing a chooser that excludes the current app to avoid + * circular loops. + */ + public void openExternalExcludeCurrentApp(Activity activity, Intent intent) { + String currentPackage = activity.getPackageName(); + boolean hasCurrentPackage = false; + PackageManager pm = activity.getPackageManager(); + List activities = pm.queryIntentActivities(intent, 0); + ArrayList targetIntents = new ArrayList(); + for (ResolveInfo ri : activities) { + if (!currentPackage.equals(ri.activityInfo.packageName)) { + Intent targetIntent = (Intent) intent.clone(); + targetIntent.setPackage(ri.activityInfo.packageName); + targetIntents.add(targetIntent); + } else { + hasCurrentPackage = true; + } + } + // If the current app package isn't a target for this URL, then use + // the normal launch behavior + if (!hasCurrentPackage || targetIntents.size() == 0) { + activity.startActivity(intent); + } + // If there's only one possible intent, launch it directly + else if (targetIntents.size() == 1) { + activity.startActivity(targetIntents.get(0)); + } + // Otherwise, show a custom chooser without the current app listed + else if (targetIntents.size() > 0) { + Intent chooser = Intent.createChooser(targetIntents.remove(targetIntents.size() - 1), null); + chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetIntents.toArray(new Parcelable[]{})); + activity.startActivity(chooser); + } + } + + public void open(Activity activity, Map arguments) { + String id = (String) arguments.get("id"); + Map urlRequest = (Map) arguments.get("urlRequest"); + String assetFilePath = (String) arguments.get("assetFilePath"); + String data = (String) arguments.get("data"); + String mimeType = (String) arguments.get("mimeType"); + String encoding = (String) arguments.get("encoding"); + String baseUrl = (String) arguments.get("baseUrl"); + String historyUrl = (String) arguments.get("historyUrl"); + Map settings = (Map) arguments.get("settings"); + Map contextMenu = (Map) arguments.get("contextMenu"); + Integer windowId = (Integer) arguments.get("windowId"); + List> initialUserScripts = (List>) arguments.get("initialUserScripts"); + Map pullToRefreshInitialSettings = (Map) arguments.get("pullToRefreshSettings"); + List> menuItems = (List>) arguments.get("menuItems"); + + Bundle extras = new Bundle(); + extras.putString("fromActivity", activity.getClass().getName()); + extras.putSerializable("initialUrlRequest", (Serializable) urlRequest); + extras.putString("initialFile", assetFilePath); + extras.putString("initialData", data); + extras.putString("initialMimeType", mimeType); + extras.putString("initialEncoding", encoding); + extras.putString("initialBaseUrl", baseUrl); + extras.putString("initialHistoryUrl", historyUrl); + extras.putString("id", id); + extras.putString("managerId", this.id); + extras.putSerializable("settings", (Serializable) settings); + extras.putSerializable("contextMenu", (Serializable) contextMenu); + extras.putInt("windowId", windowId != null ? windowId : -1); + extras.putSerializable("initialUserScripts", (Serializable) initialUserScripts); + extras.putSerializable("pullToRefreshInitialSettings", (Serializable) pullToRefreshInitialSettings); + extras.putSerializable("menuItems", (Serializable) menuItems); + startInAppBrowserActivity(activity, extras); + } + + public void startInAppBrowserActivity(Activity activity, Bundle extras) { + Intent intent = new Intent(activity, InAppBrowserActivity.class); + if (extras != null) + intent.putExtras(extras); + activity.startActivity(intent); + } + + @Override + public void dispose() { + super.dispose(); + shared.remove(this.id); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserSettings.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserSettings.java new file mode 100644 index 0000000..bdec821 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserSettings.java @@ -0,0 +1,109 @@ +package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; +import com.pichillilorenzo.flutter_inappwebview_android.R; + +import java.util.HashMap; +import java.util.Map; + +public class InAppBrowserSettings implements ISettings { + + public static final String LOG_TAG = "InAppBrowserSettings"; + + public Boolean hidden = false; + public Boolean hideToolbarTop = false; + @Nullable + public String toolbarTopBackgroundColor; + @Nullable + public String toolbarTopFixedTitle; + public Boolean hideUrlBar = false; + public Boolean hideProgressBar = false; + + public Boolean hideTitleBar = false; + public Boolean closeOnCannotGoBack = true; + public Boolean allowGoBackWithBackButton = true; + public Boolean shouldCloseOnBackButtonPressed = false; + public Boolean hideDefaultMenuItems = false; + + @NonNull + @Override + public InAppBrowserSettings parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "hidden": + hidden = (Boolean) value; + break; + case "hideToolbarTop": + hideToolbarTop = (Boolean) value; + break; + case "toolbarTopBackgroundColor": + toolbarTopBackgroundColor = (String) value; + break; + case "toolbarTopFixedTitle": + toolbarTopFixedTitle = (String) value; + break; + case "hideUrlBar": + hideUrlBar = (Boolean) value; + break; + case "hideTitleBar": + hideTitleBar = (Boolean) value; + break; + case "closeOnCannotGoBack": + closeOnCannotGoBack = (Boolean) value; + break; + case "hideProgressBar": + hideProgressBar = (Boolean) value; + break; + case "allowGoBackWithBackButton": + allowGoBackWithBackButton = (Boolean) value; + break; + case "shouldCloseOnBackButtonPressed": + shouldCloseOnBackButtonPressed = (Boolean) value; + break; + case "hideDefaultMenuItems": + hideDefaultMenuItems = (Boolean) value; + break; + } + } + + return this; + } + + @NonNull + @Override + public Map toMap() { + Map settings = new HashMap<>(); + settings.put("hidden", hidden); + settings.put("hideToolbarTop", hideToolbarTop); + settings.put("toolbarTopBackgroundColor", toolbarTopBackgroundColor); + settings.put("toolbarTopFixedTitle", toolbarTopFixedTitle); + settings.put("hideUrlBar", hideUrlBar); + settings.put("hideTitleBar", hideTitleBar); + settings.put("closeOnCannotGoBack", closeOnCannotGoBack); + settings.put("hideProgressBar", hideProgressBar); + settings.put("allowGoBackWithBackButton", allowGoBackWithBackButton); + settings.put("shouldCloseOnBackButtonPressed", shouldCloseOnBackButtonPressed); + settings.put("hideDefaultMenuItems", hideDefaultMenuItems); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull InAppBrowserActivity inAppBrowserActivity) { + Map realSettings = toMap(); + realSettings.put("hidden", inAppBrowserActivity.isHidden); + realSettings.put("hideToolbarTop", inAppBrowserActivity.actionBar == null || !inAppBrowserActivity.actionBar.isShowing()); + realSettings.put("hideUrlBar", inAppBrowserActivity.menu == null || !inAppBrowserActivity.menu.findItem(R.id.menu_search).isVisible()); + realSettings.put("hideProgressBar", inAppBrowserActivity.progressBar == null || inAppBrowserActivity.progressBar.getMax() == 0); + return realSettings; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptAjaxRequestJS.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptAjaxRequestJS.java new file mode 100644 index 0000000..2ec2e01 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptAjaxRequestJS.java @@ -0,0 +1,266 @@ +package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime; + +public class InterceptAjaxRequestJS { + + public static final String INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT"; + public static final String FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE = JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._useShouldInterceptAjaxRequest"; + public static final String FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE = JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._interceptOnlyAsyncAjaxRequests"; + public static final PluginScript INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT = new PluginScript( + InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + true, + null + ); + + public static PluginScript createInterceptOnlyAsyncAjaxRequestsPluginScript(boolean onlyAsync) { + return new PluginScript( + InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + "window." + FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE + " = " + onlyAsync +";", + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + true, + null + ); + } + + public static final String INTERCEPT_AJAX_REQUEST_JS_SOURCE = "(function(ajax) {" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " = true;" + + " var send = ajax.prototype.send;" + + " var open = ajax.prototype.open;" + + " var setRequestHeader = ajax.prototype.setRequestHeader;" + + " ajax.prototype._flutter_inappwebview_url = null;" + + " ajax.prototype._flutter_inappwebview_method = null;" + + " ajax.prototype._flutter_inappwebview_isAsync = null;" + + " ajax.prototype._flutter_inappwebview_user = null;" + + " ajax.prototype._flutter_inappwebview_password = null;" + + " ajax.prototype._flutter_inappwebview_password = null;" + + " ajax.prototype._flutter_inappwebview_already_onreadystatechange_wrapped = false;" + + " ajax.prototype._flutter_inappwebview_request_headers = {};" + + " function convertRequestResponse(request, callback) {" + + " if (request.response != null && request.responseType != null) {" + + " switch (request.responseType) {" + + " case 'arraybuffer':" + + " callback(new Uint8Array(request.response));" + + " return;" + + " case 'blob':" + + " const reader = new FileReader();" + + " reader.addEventListener('loadend', function() { " + + " callback(new Uint8Array(reader.result));" + + " });" + + " reader.readAsArrayBuffer(blob);" + + " return;" + + " case 'document':" + + " callback(request.response.documentElement.outerHTML);" + + " return;" + + " case 'json':" + + " callback(request.response);" + + " return;" + + " };" + + " }" + + " callback(null);" + + " };" + + " ajax.prototype.open = function(method, url, isAsync, user, password) {" + + " isAsync = (isAsync != null) ? isAsync : true;" + + " this._flutter_inappwebview_url = url;" + + " this._flutter_inappwebview_method = method;" + + " this._flutter_inappwebview_isAsync = isAsync;" + + " this._flutter_inappwebview_user = user;" + + " this._flutter_inappwebview_password = password;" + + " this._flutter_inappwebview_request_headers = {};" + + " open.call(this, method, url, isAsync, user, password);" + + " };" + + " ajax.prototype.setRequestHeader = function(header, value) {" + + " this._flutter_inappwebview_request_headers[header] = value;" + + " setRequestHeader.call(this, header, value);" + + " };" + + " function handleEvent(e) {" + + " var self = this;" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " if (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == true) {" + + " var headers = this.getAllResponseHeaders();" + + " var responseHeaders = {};" + + " if (headers != null) {" + + " var arr = headers.trim().split(/[\\r\\n]+/);" + + " arr.forEach(function (line) {" + + " var parts = line.split(': ');" + + " var header = parts.shift();" + + " var value = parts.join(': ');" + + " responseHeaders[header] = value;" + + " });" + + " }" + + " convertRequestResponse(this, function(response) {" + + " var ajaxRequest = {" + + " method: self._flutter_inappwebview_method," + + " url: self._flutter_inappwebview_url," + + " isAsync: self._flutter_inappwebview_isAsync," + + " user: self._flutter_inappwebview_user," + + " password: self._flutter_inappwebview_password," + + " withCredentials: self.withCredentials," + + " headers: self._flutter_inappwebview_request_headers," + + " readyState: self.readyState," + + " status: self.status," + + " responseURL: self.responseURL," + + " responseType: self.responseType," + + " response: response," + + " responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null," + + " responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null," + + " statusText: self.statusText," + + " responseHeaders, responseHeaders," + + " event: {" + + " type: e.type," + + " loaded: e.loaded," + + " lengthComputable: e.lengthComputable," + + " total: e.total" + + " }" + + " };" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onAjaxProgress', ajaxRequest).then(function(result) {" + + " if (result != null) {" + + " switch (result) {" + + " case 0:" + + " self.abort();" + + " return;" + + " };" + + " }" + + " });" + + " });" + + " }" + + " };" + + " ajax.prototype.send = function(data) {" + + " var self = this;" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " var canBeIntercepted = self._flutter_inappwebview_isAsync || w." + FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE + " === false;" + + " if (canBeIntercepted && (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == true)) {" + + " if (!this._flutter_inappwebview_already_onreadystatechange_wrapped) {" + + " this._flutter_inappwebview_already_onreadystatechange_wrapped = true;" + + " var onreadystatechange = this.onreadystatechange;" + + " this.onreadystatechange = function() {" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " if (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == true) {" + + " var headers = this.getAllResponseHeaders();" + + " var responseHeaders = {};" + + " if (headers != null) {" + + " var arr = headers.trim().split(/[\\r\\n]+/);" + + " arr.forEach(function (line) {" + + " var parts = line.split(': ');" + + " var header = parts.shift();" + + " var value = parts.join(': ');" + + " responseHeaders[header] = value;" + + " });" + + " }" + + " convertRequestResponse(this, function(response) {" + + " var ajaxRequest = {" + + " method: self._flutter_inappwebview_method," + + " url: self._flutter_inappwebview_url," + + " isAsync: self._flutter_inappwebview_isAsync," + + " user: self._flutter_inappwebview_user," + + " password: self._flutter_inappwebview_password," + + " withCredentials: self.withCredentials," + + " headers: self._flutter_inappwebview_request_headers," + + " readyState: self.readyState," + + " status: self.status," + + " responseURL: self.responseURL," + + " responseType: self.responseType," + + " response: response," + + " responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null," + + " responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null," + + " statusText: self.statusText," + + " responseHeaders: responseHeaders" + + " };" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onAjaxReadyStateChange', ajaxRequest).then(function(result) {" + + " if (result != null) {" + + " switch (result) {" + + " case 0:" + + " self.abort();" + + " return;" + + " };" + + " }" + + " if (onreadystatechange != null) {" + + " onreadystatechange();" + + " }" + + " });" + + " });" + + " } else if (onreadystatechange != null) {" + + " onreadystatechange();" + + " }" + + " };" + + " }" + + " this.addEventListener('loadstart', handleEvent);" + + " this.addEventListener('load', handleEvent);" + + " this.addEventListener('loadend', handleEvent);" + + " this.addEventListener('progress', handleEvent);" + + " this.addEventListener('error', handleEvent);" + + " this.addEventListener('abort', handleEvent);" + + " this.addEventListener('timeout', handleEvent);" + + " " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertBodyRequest(data).then(function(data) {" + + " var ajaxRequest = {" + + " data: data," + + " method: self._flutter_inappwebview_method," + + " url: self._flutter_inappwebview_url," + + " isAsync: self._flutter_inappwebview_isAsync," + + " user: self._flutter_inappwebview_user," + + " password: self._flutter_inappwebview_password," + + " withCredentials: self.withCredentials," + + " headers: self._flutter_inappwebview_request_headers," + + " responseType: self.responseType" + + " };" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) {" + + " if (result != null) {" + + " switch (result) {" + + " case 0:" + + " self.abort();" + + " return;" + + " };" + + " if (result.data != null && !" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.data) && result.data.length > 0) {" + + " var bodyString = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".arrayBufferToString(result.data);" + + " if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isBodyFormData(bodyString)) {" + + " var formDataContentType = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".getFormDataContentType(bodyString);" + + " if (result.headers != null) {" + + " result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type'];" + + " } else {" + + " result.headers = { 'Content-Type': formDataContentType };" + + " }" + + " }" + + " }" + + " if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.data) || result.data == null) {" + + " data = result.data;" + + " } else if (result.data.length > 0) {" + + " data = new Uint8Array(result.data);" + + " }" + + " self.withCredentials = result.withCredentials;" + + " if (result.responseType != null && self._flutter_inappwebview_isAsync) {" + + " self.responseType = result.responseType;" + + " };" + + " for (var header in result.headers) {" + + " var value = result.headers[header];" + + " var flutter_inappwebview_value = self._flutter_inappwebview_request_headers[header];" + + " if (flutter_inappwebview_value == null) {" + + " self._flutter_inappwebview_request_headers[header] = value;" + + " } else {" + + " self._flutter_inappwebview_request_headers[header] += ', ' + value;" + + " }" + + " setRequestHeader.call(self, header, value);" + + " };" + + " if ((self._flutter_inappwebview_method != result.method && result.method != null) ||" + + " (self._flutter_inappwebview_url != result.url && result.url != null) ||" + + " (self._flutter_inappwebview_isAsync != result.isAsync && result.isAsync != null) ||" + + " (self._flutter_inappwebview_user != result.user && result.user != null) ||" + + " (self._flutter_inappwebview_password != result.password && result.password != null)) {" + + " self.abort();" + + " self.open(result.method, result.url, result.isAsync, result.user, result.password);" + + " }" + + " }" + + " send.call(self, data);" + + " });" + + " });" + + " } else {" + + " send.call(this, data);" + + " }" + + " };" + + "})(window.XMLHttpRequest);"; +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptFetchRequestJS.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptFetchRequestJS.java new file mode 100644 index 0000000..eebff12 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptFetchRequestJS.java @@ -0,0 +1,152 @@ +package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime; + +public class InterceptFetchRequestJS { + + public static final String INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT"; + public static final String FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE = JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._useShouldInterceptFetchRequest"; + public static final PluginScript INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT = new PluginScript( + InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + true, + null + ); + + public static final String INTERCEPT_FETCH_REQUEST_JS_SOURCE = "(function(fetch) {" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE + " = true;" + + " if (fetch == null) {" + + " return;" + + " }" + + " window.fetch = async function(resource, init) {" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " if (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE + " == true) {" + + " var fetchRequest = {" + + " url: null," + + " method: null," + + " headers: null," + + " body: null," + + " mode: null," + + " credentials: null," + + " cache: null," + + " redirect: null," + + " referrer: null," + + " referrerPolicy: null," + + " integrity: null," + + " keepalive: null" + + " };" + + " if (resource instanceof Request) {" + + " fetchRequest.url = resource.url;" + + " fetchRequest.method = resource.method;" + + " fetchRequest.headers = resource.headers;" + + " fetchRequest.body = resource.body;" + + " fetchRequest.mode = resource.mode;" + + " fetchRequest.credentials = resource.credentials;" + + " fetchRequest.cache = resource.cache;" + + " fetchRequest.redirect = resource.redirect;" + + " fetchRequest.referrer = resource.referrer;" + + " fetchRequest.referrerPolicy = resource.referrerPolicy;" + + " fetchRequest.integrity = resource.integrity;" + + " fetchRequest.keepalive = resource.keepalive;" + + " } else {" + + " fetchRequest.url = resource != null ? resource.toString() : null;" + + " if (init != null) {" + + " fetchRequest.method = init.method;" + + " fetchRequest.headers = init.headers;" + + " fetchRequest.body = init.body;" + + " fetchRequest.mode = init.mode;" + + " fetchRequest.credentials = init.credentials;" + + " fetchRequest.cache = init.cache;" + + " fetchRequest.redirect = init.redirect;" + + " fetchRequest.referrer = init.referrer;" + + " fetchRequest.referrerPolicy = init.referrerPolicy;" + + " fetchRequest.integrity = init.integrity;" + + " fetchRequest.keepalive = init.keepalive;" + + " }" + + " }" + + " if (fetchRequest.headers instanceof Headers) {" + + " fetchRequest.headers = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertHeadersToJson(fetchRequest.headers);" + + " }" + + " fetchRequest.credentials = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertCredentialsToJson(fetchRequest.credentials);" + + " return " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertBodyRequest(fetchRequest.body).then(function(body) {" + + " fetchRequest.body = body;" + + " return window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('shouldInterceptFetchRequest', fetchRequest).then(function(result) {" + + " if (result != null) {" + + " switch (result.action) {" + + " case 0:" + + " var controller = new AbortController();" + + " if (init != null) {" + + " init.signal = controller.signal;" + + " } else {" + + " init = {" + + " signal: controller.signal" + + " };" + + " }" + + " controller.abort();" + + " break;" + + " }" + + " if (result.body != null && !" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.body) && result.body.length > 0) {" + + " var bodyString = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".arrayBufferToString(result.body);" + + " if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isBodyFormData(bodyString)) {" + + " var formDataContentType = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".getFormDataContentType(bodyString);" + + " if (result.headers != null) {" + + " result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type'];" + + " } else {" + + " result.headers = { 'Content-Type': formDataContentType };" + + " }" + + " }" + + " }" + + " resource = result.url;" + + " if (init == null) {" + + " init = {};" + + " }" + + " if (result.method != null && result.method.length > 0) {" + + " init.method = result.method;" + + " }" + + " if (result.headers != null && Object.keys(result.headers).length > 0) {" + + " init.headers = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertJsonToHeaders(result.headers);" + + " }" + + " if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.body) || result.body == null) {" + + " init.body = result.body;" + + " } else if (result.body.length > 0) {" + + " init.body = new Uint8Array(result.body);" + + " }" + + " if (result.mode != null && result.mode.length > 0) {" + + " init.mode = result.mode;" + + " }" + + " if (result.credentials != null) {" + + " init.credentials = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertJsonToCredential(result.credentials);" + + " }" + + " if (result.cache != null && result.cache.length > 0) {" + + " init.cache = result.cache;" + + " }" + + " if (result.redirect != null && result.redirect.length > 0) {" + + " init.redirect = result.redirect;" + + " }" + + " if (result.referrer != null && result.referrer.length > 0) {" + + " init.referrer = result.referrer;" + + " }" + + " if (result.referrerPolicy != null && result.referrerPolicy.length > 0) {" + + " init.referrerPolicy = result.referrerPolicy;" + + " }" + + " if (result.integrity != null && result.integrity.length > 0) {" + + " init.integrity = result.integrity;" + + " }" + + " if (result.keepalive != null) {" + + " init.keepalive = result.keepalive;" + + " }" + + " return fetch(resource, init);" + + " }" + + " return fetch(resource, init);" + + " });" + + " });" + + " } else {" + + " return fetch(resource, init);" + + " }" + + " };" + + "})(window.fetch);"; +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/JavaScriptBridgeJS.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/JavaScriptBridgeJS.java new file mode 100644 index 0000000..55659d4 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/JavaScriptBridgeJS.java @@ -0,0 +1,250 @@ +package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime; + +public class JavaScriptBridgeJS { + public static final String JAVASCRIPT_BRIDGE_NAME = "flutter_inappwebview"; + public static final String JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT"; + public static final PluginScript JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT = new PluginScript( + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME, + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + true, + null + ); + + public static final String JAVASCRIPT_UTIL_VAR_NAME = "window." + JAVASCRIPT_BRIDGE_NAME + "._Util"; + public static final String WEB_MESSAGE_CHANNELS_VARIABLE_NAME = "window." + JAVASCRIPT_BRIDGE_NAME + "._webMessageChannels"; + + public static final String UTIL_JS_SOURCE = JAVASCRIPT_UTIL_VAR_NAME + " = {" + + " support: {" + + " searchParams: 'URLSearchParams' in window," + + " iterable: 'Symbol' in window && 'iterator' in Symbol," + + " blob:" + + " 'FileReader' in window &&" + + " 'Blob' in window &&" + + " (function() {" + + " try {" + + " new Blob();" + + " return true;" + + " } catch (e) {" + + " return false;" + + " }" + + " })()," + + " formData: 'FormData' in window," + + " arrayBuffer: 'ArrayBuffer' in window" + + " }," + + " isDataView: function(obj) {" + + " return obj && DataView.prototype.isPrototypeOf(obj);" + + " }," + + " fileReaderReady: function(reader) {" + + " return new Promise(function(resolve, reject) {" + + " reader.onload = function() {" + + " resolve(reader.result);" + + " };" + + " reader.onerror = function() {" + + " reject(reader.error);" + + " };" + + " });" + + " }," + + " readBlobAsArrayBuffer: function(blob) {" + + " var reader = new FileReader();" + + " var promise = " + JAVASCRIPT_UTIL_VAR_NAME + ".fileReaderReady(reader);" + + " reader.readAsArrayBuffer(blob);" + + " return promise;" + + " }," + + " convertBodyToArrayBuffer: function(body) {" + + " var viewClasses = [" + + " '[object Int8Array]'," + + " '[object Uint8Array]'," + + " '[object Uint8ClampedArray]'," + + " '[object Int16Array]'," + + " '[object Uint16Array]'," + + " '[object Int32Array]'," + + " '[object Uint32Array]'," + + " '[object Float32Array]'," + + " '[object Float64Array]'" + + " ];" + + " var isArrayBufferView = null;" + + " if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.arrayBuffer) {" + + " isArrayBufferView =" + + " ArrayBuffer.isView ||" + + " function(obj) {" + + " return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1;" + + " };" + + " }" + + " var bodyUsed = false;" + + " this._bodyInit = body;" + + " if (!body) {" + + " this._bodyText = '';" + + " } else if (typeof body === 'string') {" + + " this._bodyText = body;" + + " } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.blob && Blob.prototype.isPrototypeOf(body)) {" + + " this._bodyBlob = body;" + + " } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.formData && FormData.prototype.isPrototypeOf(body)) {" + + " this._bodyFormData = body;" + + " } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {" + + " this._bodyText = body.toString();" + + " } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.arrayBuffer && " + JAVASCRIPT_UTIL_VAR_NAME + ".support.blob && " + JAVASCRIPT_UTIL_VAR_NAME + ".isDataView(body)) {" + + " this._bodyArrayBuffer = bufferClone(body.buffer);" + + " this._bodyInit = new Blob([this._bodyArrayBuffer]);" + + " } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {" + + " this._bodyArrayBuffer = bufferClone(body);" + + " } else {" + + " this._bodyText = body = Object.prototype.toString.call(body);" + + " }" + + " this.blob = function () {" + + " if (bodyUsed) {" + + " return Promise.reject(new TypeError('Already read'));" + + " }" + + " bodyUsed = true;" + + " if (this._bodyBlob) {" + + " return Promise.resolve(this._bodyBlob);" + + " } else if (this._bodyArrayBuffer) {" + + " return Promise.resolve(new Blob([this._bodyArrayBuffer]));" + + " } else if (this._bodyFormData) {" + + " throw new Error('could not read FormData body as blob');" + + " } else {" + + " return Promise.resolve(new Blob([this._bodyText]));" + + " }" + + " };" + + " if (this._bodyArrayBuffer) {" + + " if (bodyUsed) {" + + " return Promise.reject(new TypeError('Already read'));" + + " }" + + " bodyUsed = true;" + + " if (ArrayBuffer.isView(this._bodyArrayBuffer)) {" + + " return Promise.resolve(" + + " this._bodyArrayBuffer.buffer.slice(" + + " this._bodyArrayBuffer.byteOffset," + + " this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength" + + " )" + + " );" + + " } else {" + + " return Promise.resolve(this._bodyArrayBuffer);" + + " }" + + " }" + + " return this.blob().then(" + JAVASCRIPT_UTIL_VAR_NAME + ".readBlobAsArrayBuffer);" + + " }," + + " isString: function(variable) {" + + " return typeof variable === 'string' || variable instanceof String;" + + " }," + + " convertBodyRequest: function(body) {" + + " if (body == null) {" + + " return new Promise(function(resolve, reject) { resolve(null); });" + + " }" + + " if (" + JAVASCRIPT_UTIL_VAR_NAME + ".isString(body) || (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.searchParams && body instanceof URLSearchParams)) {" + + " return new Promise(function(resolve, reject) { resolve(body.toString()); });" + + " }" + + " if (window.Response != null) {" + + " return new Response(body).arrayBuffer().then(function(arrayBuffer) {" + + " return Array.from(new Uint8Array(arrayBuffer));" + + " });" + + " }" + + " return " + JAVASCRIPT_UTIL_VAR_NAME + ".convertBodyToArrayBuffer(body).then(function(arrayBuffer) {" + + " return Array.from(new Uint8Array(arrayBuffer));" + + " });" + + " }," + + " arrayBufferToString: function(arrayBuffer) {" + + " var uint8Array = new Uint8Array(arrayBuffer);" + + " return uint8Array.reduce(function(acc, i) { return acc += String.fromCharCode.apply(null, [i]); }, '');" + + " }," + + " isBodyFormData: function(bodyString) {" + + " return bodyString.indexOf('------WebKitFormBoundary') >= 0;" + + " }," + + " getFormDataContentType: function(bodyString) {" + + " var boundary = bodyString.substr(2, 40);" + + " return 'multipart/form-data; boundary=' + boundary;" + + " }," + + " convertHeadersToJson: function(headers) {" + + " var headersObj = {};" + + " for (var header of headers.keys()) {" + + " var value = headers.get(header);" + + " headersObj[header] = value;" + + " }" + + " return headersObj;" + + " }," + + " convertJsonToHeaders: function(headersJson) {" + + " return new Headers(headersJson);" + + " }," + + " convertCredentialsToJson: function(credentials) {" + + " var credentialsObj = {};" + + " if (window.FederatedCredential != null && credentials instanceof FederatedCredential) {" + + " credentialsObj.type = credentials.type;" + + " credentialsObj.id = credentials.id;" + + " credentialsObj.name = credentials.name;" + + " credentialsObj.protocol = credentials.protocol;" + + " credentialsObj.provider = credentials.provider;" + + " credentialsObj.iconURL = credentials.iconURL;" + + " } else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) {" + + " credentialsObj.type = credentials.type;" + + " credentialsObj.id = credentials.id;" + + " credentialsObj.name = credentials.name;" + + " credentialsObj.password = credentials.password;" + + " credentialsObj.iconURL = credentials.iconURL;" + + " } else {" + + " credentialsObj.type = 'default';" + + " credentialsObj.value = credentials;" + + " }" + + " return credentialsObj;" + + " }," + + " convertJsonToCredential: function(credentialsJson) {" + + " var credentials;" + + " if (window.FederatedCredential != null && credentialsJson.type === 'federated') {" + + " credentials = new FederatedCredential({" + + " id: credentialsJson.id," + + " name: credentialsJson.name," + + " protocol: credentialsJson.protocol," + + " provider: credentialsJson.provider," + + " iconURL: credentialsJson.iconURL" + + " });" + + " } else if (window.PasswordCredential != null && credentialsJson.type === 'password') {" + + " credentials = new PasswordCredential({" + + " id: credentialsJson.id," + + " name: credentialsJson.name," + + " password: credentialsJson.password," + + " iconURL: credentialsJson.iconURL" + + " });" + + " } else {" + + " credentials = credentialsJson.value == null ? undefined : credentialsJson.value;" + + " }" + + " return credentials;" + + " }" + + "};"; + + public static final String JAVASCRIPT_BRIDGE_JS_SOURCE = "if (window." + JAVASCRIPT_BRIDGE_NAME + " != null) {" + + " window." + JAVASCRIPT_BRIDGE_NAME + ".callHandler = function() {" + + " var _callHandlerID = setTimeout(function(){});" + + " window." + JAVASCRIPT_BRIDGE_NAME + "._callHandler(arguments[0], _callHandlerID, JSON.stringify(Array.prototype.slice.call(arguments, 1)));" + + " return new Promise(function(resolve, reject) {" + + " window." + JAVASCRIPT_BRIDGE_NAME + "[_callHandlerID] = {resolve: resolve, reject: reject};" + + " });" + + " };" + + "}"+ + "if (window.top != null && window.top !== window && window." + JAVASCRIPT_BRIDGE_NAME + " == null) {" + + " window." + JAVASCRIPT_BRIDGE_NAME + " = {};" + + " window." + JAVASCRIPT_BRIDGE_NAME + ".callHandler = function() {" + + " var _callHandlerID = setTimeout(function(){});" + + " try {" + + " window.top." + JAVASCRIPT_BRIDGE_NAME + "._callHandler(arguments[0], _callHandlerID, JSON.stringify(Array.prototype.slice.call(arguments, 1)));" + + " return new Promise(function(resolve, reject) {" + + " window.top." + JAVASCRIPT_BRIDGE_NAME + "[_callHandlerID] = {resolve: resolve, reject: reject};" + + " });" + + " } catch (error) {" + + " return new Promise(function(resolve, reject) { reject(error); });" + + " }" + + " };" + + "}" + + "if (window." + JAVASCRIPT_BRIDGE_NAME + " != null) {" + + " " + UTIL_JS_SOURCE + + "}"; + + public static final String PLATFORM_READY_JS_SOURCE = "(function() {" + + " if ((window.top == null || window.top === window) && window." + JAVASCRIPT_BRIDGE_NAME + " != null && window." + JAVASCRIPT_BRIDGE_NAME + "._platformReady == null) {" + + " window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));" + + " window." + JAVASCRIPT_BRIDGE_NAME + "._platformReady = true;" + + " }" + + "})();"; +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnLoadResourceJS.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnLoadResourceJS.java new file mode 100644 index 0000000..b386bd7 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnLoadResourceJS.java @@ -0,0 +1,35 @@ +package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime; + +public class OnLoadResourceJS { + public static final String ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT"; + public static final String FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE = JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._useOnLoadResource"; + public static final PluginScript ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT = new PluginScript( + OnLoadResourceJS.ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME, + OnLoadResourceJS.ON_LOAD_RESOURCE_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false, + null + ); + + public static final String ON_LOAD_RESOURCE_JS_SOURCE = "window." + FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE + " = true;" + + "(function() {" + + " var observer = new PerformanceObserver(function(list) {" + + " list.getEntries().forEach(function(entry) {" + + " if (" + FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE + " == null || " + FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE + " == true) {" + + " var resource = {" + + " 'url': entry.name," + + " 'initiatorType': entry.initiatorType," + + " 'startTime': entry.startTime," + + " 'duration': entry.duration" + + " };" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onLoadResource', resource);" + + " }" + + " });" + + " });" + + " observer.observe({entryTypes: ['resource']});" + + "})();"; +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowBlurEventJS.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowBlurEventJS.java new file mode 100644 index 0000000..746f878 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowBlurEventJS.java @@ -0,0 +1,22 @@ +package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime; + +public class OnWindowBlurEventJS { + public static final String ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT"; + public static final PluginScript ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT = new PluginScript( + OnWindowBlurEventJS.ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + OnWindowBlurEventJS.ON_WINDOW_BLUR_EVENT_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false, + null + ); + + public static final String ON_WINDOW_BLUR_EVENT_JS_SOURCE = "(function(){" + + " window.addEventListener('blur', function(e) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onWindowBlur');" + + " });" + + "})();"; +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowFocusEventJS.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowFocusEventJS.java new file mode 100644 index 0000000..cfdae9a --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowFocusEventJS.java @@ -0,0 +1,22 @@ +package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime; + +public class OnWindowFocusEventJS { + public static final String ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT"; + public static final PluginScript ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT = new PluginScript( + OnWindowFocusEventJS.ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + OnWindowFocusEventJS.ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false, + null + ); + + public static final String ON_WINDOW_FOCUS_EVENT_JS_SOURCE = "(function(){" + + " window.addEventListener('focus', function(e) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onWindowFocus');" + + " });" + + "})();"; +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PluginScriptsUtil.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PluginScriptsUtil.java new file mode 100644 index 0000000..91af1fd --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PluginScriptsUtil.java @@ -0,0 +1,89 @@ +package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime; + +public class PluginScriptsUtil { + + public static final String VAR_PLACEHOLDER_VALUE = "$IN_APP_WEBVIEW_PLACEHOLDER_VALUE"; + public static final String VAR_CONTENT_WORLD_NAME_ARRAY = "$IN_APP_WEBVIEW_CONTENT_WORLD_NAME_ARRAY"; + public static final String VAR_CONTENT_WORLD_NAME = "$IN_APP_WEBVIEW_CONTENT_WORLD_NAME"; + public static final String VAR_JSON_SOURCE_ENCODED = "$IN_APP_WEBVIEW_JSON_SOURCE_ENCODED"; + public static final String VAR_FUNCTION_ARGUMENT_NAMES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_NAMES"; + public static final String VAR_FUNCTION_ARGUMENT_VALUES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_VALUES"; + public static final String VAR_FUNCTION_ARGUMENTS_OBJ = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENTS_OBJ"; + public static final String VAR_FUNCTION_BODY = "$IN_APP_WEBVIEW_FUNCTION_BODY"; + public static final String VAR_RESULT_UUID = "$IN_APP_WEBVIEW_RESULT_UUID"; + public static final String VAR_RANDOM_NAME = "$IN_APP_WEBVIEW_VARIABLE_RANDOM_NAME"; + + public static final String CALL_ASYNC_JAVA_SCRIPT_WRAPPER_JS_SOURCE = "(function(obj) {" + + " (async function(" + VAR_FUNCTION_ARGUMENT_NAMES + ") {" + + " \n" + VAR_FUNCTION_BODY + "\n" + + " })(" + VAR_FUNCTION_ARGUMENT_VALUES + ").then(function(value) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('callAsyncJavaScript', {'value': value, 'error': null, 'resultUuid': '" + VAR_RESULT_UUID + "'});" + + " }).catch(function(error) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('callAsyncJavaScript', {'value': null, 'error': error + '', 'resultUuid': '" + VAR_RESULT_UUID + "'});" + + " });" + + " return null;" + + "})(" + VAR_FUNCTION_ARGUMENTS_OBJ + ");"; + + public static final String EVALUATE_JAVASCRIPT_WITH_CONTENT_WORLD_WRAPPER_JS_SOURCE = "var $IN_APP_WEBVIEW_VARIABLE_RANDOM_NAME = null;" + + "try {" + + " $IN_APP_WEBVIEW_VARIABLE_RANDOM_NAME = eval(" + VAR_PLACEHOLDER_VALUE + ");" + + "} catch(e) {" + + " console.error(e);" + + "}" + + "window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('evaluateJavaScriptWithContentWorld', {'value': $IN_APP_WEBVIEW_VARIABLE_RANDOM_NAME, 'resultUuid': '" + VAR_RESULT_UUID + "'});"; + + public static final String IS_ACTIVE_ELEMENT_INPUT_EDITABLE_JS_SOURCE = + "var activeEl = document.activeElement;" + + "var nodeName = (activeEl != null) ? activeEl.nodeName.toLowerCase() : '';" + + "var isActiveElementInputEditable = activeEl != null && " + + "(activeEl.nodeType == 1 && (nodeName == 'textarea' || (nodeName == 'input' && /^(?:text|email|number|search|tel|url|password)$/i.test(activeEl.type != null ? activeEl.type : 'text')))) && " + + "!activeEl.disabled && !activeEl.readOnly;" + + "var isActiveElementEditable = isActiveElementInputEditable || (activeEl != null && activeEl.isContentEditable) || document.designMode === 'on';"; + + // android Workaround to hide context menu when selected text is empty + // and the document active element is not an input element. + public static final String CHECK_CONTEXT_MENU_SHOULD_BE_HIDDEN_JS_SOURCE = "(function(){" + + " var txt;" + + " if (window.getSelection) {" + + " txt = window.getSelection().toString();" + + " } else if (window.document.getSelection) {" + + " txt = window.document.getSelection().toString();" + + " } else if (window.document.selection) {" + + " txt = window.document.selection.createRange().text;" + + " }" + + IS_ACTIVE_ELEMENT_INPUT_EDITABLE_JS_SOURCE + + " return txt === '' && !isActiveElementEditable;" + + "})();"; + + public static final String GET_SELECTED_TEXT_JS_SOURCE = "(function(){" + + " var txt;" + + " if (window.getSelection) {" + + " txt = window.getSelection().toString();" + + " } else if (window.document.getSelection) {" + + " txt = window.document.getSelection().toString();" + + " } else if (window.document.selection) {" + + " txt = window.document.selection.createRange().text;" + + " }" + + " return txt;" + + "})();"; + + public static final String CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT_GROUP_NAME = "CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT"; + public static final PluginScript CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT = new PluginScript( + PluginScriptsUtil.CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT_GROUP_NAME, + PluginScriptsUtil.CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false, + null + ); + + // android Workaround to hide context menu when user emit a keydown event + public static final String CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_SOURCE = "(function(){" + + " document.addEventListener('keydown', function(e) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._hideContextMenu();" + + " });" + + "})();"; +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PrintJS.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PrintJS.java new file mode 100644 index 0000000..bb10848 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PrintJS.java @@ -0,0 +1,24 @@ +package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime; + +public class PrintJS { + public static final String PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PRINT_JS_PLUGIN_SCRIPT"; + public static final PluginScript PRINT_JS_PLUGIN_SCRIPT = new PluginScript( + PrintJS.PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + PrintJS.PRINT_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false, + null + ); + + public static final String PRINT_JS_SOURCE = "window.print = function() {" + + " if (window.top == null || window.top === window) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onPrintRequest', window.location.href);" + + " } else {" + + " window.top.print();" + + " }" + + "};"; +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PromisePolyfillJS.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PromisePolyfillJS.java new file mode 100644 index 0000000..af10c5d --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PromisePolyfillJS.java @@ -0,0 +1,21 @@ +package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime; + +public class PromisePolyfillJS { + public static final String PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PROMISE_POLYFILL_JS_PLUGIN_SCRIPT"; + public static final PluginScript PROMISE_POLYFILL_JS_PLUGIN_SCRIPT = new PluginScript( + PromisePolyfillJS.PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME, + PromisePolyfillJS.PROMISE_POLYFILL_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + true, + null + ); + + // https://github.com/tildeio/rsvp.js + public static final String PROMISE_POLYFILL_JS_SOURCE = "if (window.Promise == null) {" + + " !function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?e(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],e):e(t.RSVP={})}(this,function(t){\"use strict\";function e(t){var e=t._promiseCallbacks;return e||(e=t._promiseCallbacks={}),e}var r={mixin:function(t){return t.on=this.on,t.off=this.off,t.trigger=this.trigger,t._promiseCallbacks=void 0,t},on:function(t,r){if(\"function\"!=typeof r)throw new TypeError(\"Callback must be a function\");var n=e(this),o=n[t];o||(o=n[t]=[]),-1===o.indexOf(r)&&o.push(r)},off:function(t,r){var n=e(this);if(r){var o=n[t],i=o.indexOf(r);-1!==i&&o.splice(i,1)}else n[t]=[]},trigger:function(t,r,n){var o=e(this)[t];if(o)for(var i=0;i2&&void 0!==arguments[2])||arguments[2],o=arguments[3];return function(t,e){if(!t)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return!e||\"object\"!=typeof e&&\"function\"!=typeof e?t:e}(this,t.call(this,e,r,n,o))}return function(t,e){if(\"function\"!=typeof e&&null!==e)throw new TypeError(\"Super expression must either be null or a function, not \"+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype._init=function(t,e){this._result={},this._enumerate(e)},e.prototype._enumerate=function(t){var e=Object.keys(t),r=e.length,n=this.promise;this._remaining=r;for(var o=void 0,i=void 0,s=0;n._state===a&&s jobs = new HashMap<>(); + + public PrintJobManager(@NonNull final InAppWebViewFlutterPlugin plugin) { + super(); + this.plugin = plugin; + } + + public void dispose() { + Collection printJobControllers = jobs.values(); + for (PrintJobController job : printJobControllers) { + if (job != null) { + job.dispose(); + } + } + jobs.clear(); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/print_job/PrintJobSettings.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/print_job/PrintJobSettings.java new file mode 100644 index 0000000..7df47bb --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/print_job/PrintJobSettings.java @@ -0,0 +1,99 @@ +package com.pichillilorenzo.flutter_inappwebview_android.print_job; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; +import com.pichillilorenzo.flutter_inappwebview_android.types.MediaSizeExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.ResolutionExt; + +import java.util.HashMap; +import java.util.Map; + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class PrintJobSettings implements ISettings { + + public static final String LOG_TAG = "PrintJobSettings"; + + public Boolean handledByClient = false; + @Nullable + public String jobName; + @Nullable + public Integer orientation; +// @Nullable +// public MarginsExt margins; + @Nullable + public MediaSizeExt mediaSize; + @Nullable + public Integer colorMode; + @Nullable + public Integer duplexMode; + @Nullable + public ResolutionExt resolution; + + @NonNull + @Override + public PrintJobSettings parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "handledByClient": + handledByClient = (Boolean) value; + break; + case "jobName": + jobName = (String) value; + break; + case "orientation": + orientation = (Integer) value; + break; +// case "margins": +// margins = MarginsExt.fromMap((Map) value); +// break; + case "mediaSize": + mediaSize = MediaSizeExt.fromMap((Map) value); + break; + case "colorMode": + colorMode = (Integer) value; + break; + case "duplexMode": + duplexMode = (Integer) value; + break; + case "resolution": + resolution = ResolutionExt.fromMap((Map) value); + break; + } + } + + return this; + } + + @NonNull + @Override + public Map toMap() { + Map settings = new HashMap<>(); + settings.put("handledByClient", handledByClient); + settings.put("jobName", jobName); + settings.put("orientation", orientation); +// settings.put("margins", margins != null ? margins.toMap() : null); + settings.put("mediaSize", mediaSize != null ? mediaSize.toMap() : null); + settings.put("colorMode", colorMode); + settings.put("duplexMode", duplexMode); + settings.put("resolution", resolution != null ? resolution.toMap() : null); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull PrintJobController printJobController) { + Map realSettings = toMap(); + return realSettings; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigManager.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigManager.java new file mode 100644 index 0000000..0b63e3f --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigManager.java @@ -0,0 +1,56 @@ +package com.pichillilorenzo.flutter_inappwebview_android.process_global_config; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.ProcessGlobalConfig; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; + +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class ProcessGlobalConfigManager extends ChannelDelegateImpl { + protected static final String LOG_TAG = "ProcessGlobalConfigM"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_processglobalconfig"; + + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public ProcessGlobalConfigManager(@NonNull final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.plugin = plugin; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + switch (call.method) { + case "apply": + if (plugin != null && plugin.activity != null) { + ProcessGlobalConfigSettings settings = (new ProcessGlobalConfigSettings()) + .parse((Map) call.argument("settings")); + try { + ProcessGlobalConfig.apply(settings.toProcessGlobalConfig(plugin.activity)); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, "", e); + } + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + @Override + public void dispose() { + super.dispose(); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigSettings.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigSettings.java new file mode 100644 index 0000000..0736be3 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigSettings.java @@ -0,0 +1,119 @@ +package com.pichillilorenzo.flutter_inappwebview_android.process_global_config; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.ProcessGlobalConfig; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public class ProcessGlobalConfigSettings implements ISettings { + public static final String LOG_TAG = "ProcessGlobalConfigSettings"; + + @Nullable + public String dataDirectorySuffix; + @Nullable + public DirectoryBasePaths directoryBasePaths; + + @NonNull + @Override + public ProcessGlobalConfigSettings parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "dataDirectorySuffix": + dataDirectorySuffix = (String) value; + break; + case "directoryBasePaths": + directoryBasePaths = (new DirectoryBasePaths()).parse((Map) value); + break; + } + } + + return this; + } + + public ProcessGlobalConfig toProcessGlobalConfig(@NonNull Context context) { + ProcessGlobalConfig config = new ProcessGlobalConfig(); + if (dataDirectorySuffix != null && + WebViewFeature.isStartupFeatureSupported(context, WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX)) { + config.setDataDirectorySuffix(context, dataDirectorySuffix); + } + if (directoryBasePaths != null && + WebViewFeature.isStartupFeatureSupported(context, WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS)) { + config.setDirectoryBasePaths(context, + new File(directoryBasePaths.dataDirectoryBasePath), + new File(directoryBasePaths.cacheDirectoryBasePath)); + } + return config; + } + @NonNull + public Map toMap() { + Map settings = new HashMap<>(); + settings.put("dataDirectorySuffix", dataDirectorySuffix); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull ProcessGlobalConfig processGlobalConfig) { + Map realSettings = toMap(); + return realSettings; + } + + static class DirectoryBasePaths implements ISettings { + public static final String LOG_TAG = "ProcessGlobalConfigSettings"; + + public String cacheDirectoryBasePath; + public String dataDirectoryBasePath; + + @NonNull + @Override + public DirectoryBasePaths parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "cacheDirectoryBasePath": + cacheDirectoryBasePath = (String) value; + break; + case "dataDirectoryBasePath": + dataDirectoryBasePath = (String) value; + break; + } + } + + return this; + } + + @NonNull + public Map toMap() { + Map settings = new HashMap<>(); + settings.put("cacheDirectoryBasePath", cacheDirectoryBasePath); + settings.put("dataDirectoryBasePath", dataDirectoryBasePath); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull Object obj) { + Map realSettings = toMap(); + return realSettings; + } + } +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxyManager.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxyManager.java new file mode 100644 index 0000000..a576cfe --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxyManager.java @@ -0,0 +1,129 @@ +package com.pichillilorenzo.flutter_inappwebview_android.proxy; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.ProxyConfig; +import androidx.webkit.ProxyController; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.ProxyRuleExt; + +import java.util.HashMap; +import java.util.concurrent.Executor; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class ProxyManager extends ChannelDelegateImpl { + protected static final String LOG_TAG = "ProxyManager"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_proxycontroller"; + + @Nullable + public static ProxyController proxyController; + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public ProxyManager(@NonNull final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.plugin = plugin; + } + + public static void init() { + if (proxyController == null && + WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { + proxyController = ProxyController.getInstance(); + } + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + init(); + + switch (call.method) { + case "setProxyOverride": + if (proxyController != null) { + HashMap settingsMap = (HashMap) call.argument("settings"); + ProxySettings settings = new ProxySettings(); + if (settingsMap != null) { + settings.parse(settingsMap); + } + setProxyOverride(settings, result); + } else { + result.success(false); + } + break; + case "clearProxyOverride": + if (proxyController != null ) { + clearProxyOverride(result); + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + private void setProxyOverride(ProxySettings settings, final MethodChannel.Result result) { + if (proxyController != null) { + ProxyConfig.Builder proxyConfigBuilder = new ProxyConfig.Builder(); + for (String bypassRule : settings.bypassRules) { + proxyConfigBuilder.addBypassRule(bypassRule); + } + for (String direct : settings.directs) { + proxyConfigBuilder.addDirect(direct); + } + for (ProxyRuleExt proxyRule : settings.proxyRules) { + if (proxyRule.getSchemeFilter() != null) { + proxyConfigBuilder.addProxyRule(proxyRule.getUrl(), proxyRule.getSchemeFilter()); + } else { + proxyConfigBuilder.addProxyRule(proxyRule.getUrl()); + } + } + if (settings.bypassSimpleHostnames != null && settings.bypassSimpleHostnames) { + proxyConfigBuilder.bypassSimpleHostnames(); + } + if (settings.removeImplicitRules != null && settings.removeImplicitRules) { + proxyConfigBuilder.removeImplicitRules(); + } + if (settings.reverseBypassEnabled != null && WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS)) { + proxyConfigBuilder.setReverseBypassEnabled(settings.reverseBypassEnabled); + } + proxyController.setProxyOverride(proxyConfigBuilder.build(), new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }, new Runnable() { + @Override + public void run() { + result.success(true); + } + }); + } + } + + private void clearProxyOverride(final MethodChannel.Result result) { + if (proxyController != null) { + proxyController.clearProxyOverride(new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }, new Runnable() { + @Override + public void run() { + result.success(true); + } + }); + } + } + + @Override + public void dispose() { + super.dispose(); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxySettings.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxySettings.java new file mode 100644 index 0000000..4834131 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxySettings.java @@ -0,0 +1,98 @@ +package com.pichillilorenzo.flutter_inappwebview_android.proxy; + +import androidx.annotation.NonNull; +import androidx.webkit.ProxyConfig; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; +import com.pichillilorenzo.flutter_inappwebview_android.types.ProxyRuleExt; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ProxySettings implements ISettings { + List bypassRules = new ArrayList<>(); + List directs = new ArrayList<>(); + List proxyRules = new ArrayList<>(); + Boolean bypassSimpleHostnames = null; + Boolean removeImplicitRules = null; + Boolean reverseBypassEnabled = false; + + @NonNull + @Override + public ProxySettings parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "bypassRules": + bypassRules = (List) value; + break; + case "directs": + directs = (List) value; + break; + case "proxyRules": + proxyRules = new ArrayList<>(); + List> proxyRuleMapList = (List>) value; + for (Map proxyRuleMap : proxyRuleMapList) { + ProxyRuleExt proxyRuleExt = ProxyRuleExt.fromMap(proxyRuleMap); + if (proxyRuleExt != null) { + proxyRules.add(proxyRuleExt); + } + } + break; + case "bypassSimpleHostnames": + bypassSimpleHostnames = (Boolean) value; + break; + case "removeImplicitRules": + removeImplicitRules = (Boolean) value; + break; + case "reverseBypassEnabled": + reverseBypassEnabled = (Boolean) value; + break; + } + } + + return this; + } + + @NonNull + @Override + public Map toMap() { + List> proxyRuleMapList = new ArrayList<>(); + for (ProxyRuleExt proxyRuleExt : proxyRules) { + proxyRuleMapList.add(proxyRuleExt.toMap()); + } + Map settings = new HashMap<>(); + settings.put("bypassRules", bypassRules); + settings.put("directs", directs); + settings.put("proxyRules", proxyRuleMapList); + settings.put("bypassSimpleHostnames", bypassSimpleHostnames); + settings.put("removeImplicitRules", removeImplicitRules); + settings.put("reverseBypassEnabled", reverseBypassEnabled); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull ProxyConfig proxyConfig) { + Map realSettings = toMap(); + List> proxyRuleMapList = new ArrayList<>(); + List proxyRules = proxyConfig.getProxyRules(); + for (ProxyConfig.ProxyRule proxyRule : proxyRules) { + Map proxyRuleMap = new HashMap<>(); + proxyRuleMap.put("url", proxyRule.getUrl()); + proxyRuleMap.put("schemeFilter", proxyRule.getSchemeFilter()); + proxyRuleMapList.add(proxyRuleMap); + } + realSettings.put("bypassRules", proxyConfig.getBypassRules()); + realSettings.put("proxyRules", proxyRuleMapList); + realSettings.put("reverseBypassEnabled", proxyConfig.isReverseBypassEnabled()); + return realSettings; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshChannelDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshChannelDelegate.java new file mode 100644 index 0000000..8ec3eb5 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshChannelDelegate.java @@ -0,0 +1,123 @@ +package com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh; + +import android.graphics.Color; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class PullToRefreshChannelDelegate extends ChannelDelegateImpl { + @Nullable + private PullToRefreshLayout pullToRefreshView; + + public PullToRefreshChannelDelegate(@NonNull PullToRefreshLayout pullToRefreshView, @NonNull MethodChannel channel) { + super(channel); + this.pullToRefreshView = pullToRefreshView; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) { + switch (call.method) { + case "setEnabled": + if (pullToRefreshView != null) { + Boolean enabled = (Boolean) call.argument("enabled"); + pullToRefreshView.settings.enabled = enabled; // used by InAppWebView.onOverScrolled + pullToRefreshView.setEnabled(enabled); + result.success(true); + } else { + result.success(false); + } + break; + case "isEnabled": + if (pullToRefreshView != null) { + result.success(pullToRefreshView.isEnabled()); + } else { + result.success(false); + } + break; + case "setRefreshing": + if (pullToRefreshView != null) { + Boolean refreshing = (Boolean) call.argument("refreshing"); + pullToRefreshView.setRefreshing(refreshing); + result.success(true); + } else { + result.success(false); + } + break; + case "isRefreshing": + result.success(pullToRefreshView != null && pullToRefreshView.isRefreshing()); + break; + case "setColor": + if (pullToRefreshView != null) { + String color = (String) call.argument("color"); + pullToRefreshView.setColorSchemeColors(Color.parseColor(color)); + result.success(true); + } else { + result.success(false); + } + break; + case "setBackgroundColor": + if (pullToRefreshView != null) { + String color = (String) call.argument("color"); + pullToRefreshView.setProgressBackgroundColorSchemeColor(Color.parseColor(color)); + result.success(true); + } else { + result.success(false); + } + break; + case "setDistanceToTriggerSync": + if (pullToRefreshView != null) { + Integer distanceToTriggerSync = (Integer) call.argument("distanceToTriggerSync"); + pullToRefreshView.setDistanceToTriggerSync(distanceToTriggerSync); + result.success(true); + } else { + result.success(false); + } + break; + case "setSlingshotDistance": + if (pullToRefreshView != null) { + Integer slingshotDistance = (Integer) call.argument("slingshotDistance"); + pullToRefreshView.setSlingshotDistance(slingshotDistance); + result.success(true); + } else { + result.success(false); + } + break; + case "getDefaultSlingshotDistance": + result.success(SwipeRefreshLayout.DEFAULT_SLINGSHOT_DISTANCE); + break; + case "setSize": + if (pullToRefreshView != null) { + Integer size = (Integer) call.argument("size"); + pullToRefreshView.setSize(size); + result.success(true); + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + public void onRefresh() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onRefresh", obj); + } + + @Override + public void dispose() { + super.dispose(); + pullToRefreshView = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshLayout.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshLayout.java new file mode 100644 index 0000000..dfbc0bf --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshLayout.java @@ -0,0 +1,86 @@ +package com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh; + +import android.content.Context; +import android.graphics.Color; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; + +import io.flutter.plugin.common.MethodChannel; + +public class PullToRefreshLayout extends SwipeRefreshLayout implements Disposable { + static final String LOG_TAG = "PullToRefreshLayout"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_"; + + @Nullable + public PullToRefreshChannelDelegate channelDelegate; + public PullToRefreshSettings settings = new PullToRefreshSettings(); + + public PullToRefreshLayout(@NonNull Context context, @NonNull InAppWebViewFlutterPlugin plugin, + @NonNull Object id, @NonNull PullToRefreshSettings settings) { + super(context); + this.settings = settings; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id); + this.channelDelegate = new PullToRefreshChannelDelegate(this, channel); + } + + public PullToRefreshLayout(@NonNull Context context) { + super(context); + } + + public PullToRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public void prepare() { + final PullToRefreshLayout self = this; + + setEnabled(settings.enabled); + setOnChildScrollUpCallback(new OnChildScrollUpCallback() { + @Override + public boolean canChildScrollUp(@NonNull SwipeRefreshLayout parent, @Nullable View child) { + if (child instanceof InAppWebView) { + InAppWebView inAppWebView = (InAppWebView) child; + return (inAppWebView.canScrollVertically() && inAppWebView.getScrollY() > 0) || + (!inAppWebView.canScrollVertically() && inAppWebView.getScrollY() == 0); + } + return true; + } + }); + setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + if (channelDelegate == null) { + self.setRefreshing(false); + return; + } + channelDelegate.onRefresh(); + } + }); + if (settings.color != null) + setColorSchemeColors(Color.parseColor(settings.color)); + if (settings.backgroundColor != null) + setProgressBackgroundColorSchemeColor(Color.parseColor(settings.backgroundColor)); + if (settings.distanceToTriggerSync != null) + setDistanceToTriggerSync(settings.distanceToTriggerSync); + if (settings.slingshotDistance != null) + setSlingshotDistance(settings.slingshotDistance); + if (settings.size != null) + setSize(settings.size); + } + + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + removeAllViews(); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshSettings.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshSettings.java new file mode 100644 index 0000000..866ed89 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshSettings.java @@ -0,0 +1,80 @@ +package com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; + +import java.util.HashMap; +import java.util.Map; + +public class PullToRefreshSettings implements ISettings { + public static final String LOG_TAG = "PullToRefreshSettings"; + + public Boolean enabled = true; + @Nullable + public String color; + @Nullable + public String backgroundColor; + @Nullable + public Integer distanceToTriggerSync; + @Nullable + public Integer slingshotDistance; + @Nullable + public Integer size; + + @NonNull + @Override + public PullToRefreshSettings parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "enabled": + enabled = (Boolean) value; + break; + case "color": + color = (String) value; + break; + case "backgroundColor": + backgroundColor = (String) value; + break; + case "distanceToTriggerSync": + distanceToTriggerSync = (Integer) value; + break; + case "slingshotDistance": + slingshotDistance = (Integer) value; + break; + case "size": + size = (Integer) value; + break; + } + } + + return this; + } + + @NonNull + public Map toMap() { + Map settings = new HashMap<>(); + settings.put("enabled", enabled); + settings.put("color", color); + settings.put("backgroundColor", backgroundColor); + settings.put("distanceToTriggerSync", distanceToTriggerSync); + settings.put("slingshotDistance", slingshotDistance); + settings.put("size", size); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull PullToRefreshLayout pullToRefreshLayout) { + Map realSettings = toMap(); + return realSettings; + } + +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerChannelDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerChannelDelegate.java new file mode 100644 index 0000000..19a4ea3 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerChannelDelegate.java @@ -0,0 +1,147 @@ +package com.pichillilorenzo.flutter_inappwebview_android.service_worker; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.webkit.ServiceWorkerControllerCompat; +import androidx.webkit.ServiceWorkerWebSettingsCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.types.BaseCallbackResultImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.SyncBaseCallbackResultImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceResponseExt; + +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +@RequiresApi(api = Build.VERSION_CODES.N) +public class ServiceWorkerChannelDelegate extends ChannelDelegateImpl { + @Nullable + private ServiceWorkerManager serviceWorkerManager; + + public ServiceWorkerChannelDelegate(@NonNull ServiceWorkerManager serviceWorkerManager, @NonNull MethodChannel channel) { + super(channel); + this.serviceWorkerManager = serviceWorkerManager; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + ServiceWorkerManager.init(); + ServiceWorkerControllerCompat serviceWorkerController = ServiceWorkerManager.serviceWorkerController; + ServiceWorkerWebSettingsCompat serviceWorkerWebSettings = (serviceWorkerController != null) ? + serviceWorkerController.getServiceWorkerWebSettings() : null; + + switch (call.method) { + case "setServiceWorkerClient": + if (serviceWorkerManager != null) { + Boolean isNull = (Boolean) call.argument("isNull"); + serviceWorkerManager.setServiceWorkerClient(isNull); + result.success(true); + } else { + result.success(false); + } + break; + case "getAllowContentAccess": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS)) { + result.success(serviceWorkerWebSettings.getAllowContentAccess()); + } else { + result.success(false); + } + break; + case "getAllowFileAccess": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_FILE_ACCESS)) { + result.success(serviceWorkerWebSettings.getAllowFileAccess()); + } else { + result.success(false); + } + break; + case "getBlockNetworkLoads": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS)) { + result.success(serviceWorkerWebSettings.getBlockNetworkLoads()); + } else { + result.success(false); + } + break; + case "getCacheMode": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_CACHE_MODE)) { + result.success(serviceWorkerWebSettings.getCacheMode()); + } else { + result.success(null); + } + break; + case "setAllowContentAccess": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS)) { + Boolean allow = (Boolean) call.argument("allow"); + serviceWorkerWebSettings.setAllowContentAccess(allow); + } + result.success(true); + break; + case "setAllowFileAccess": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_FILE_ACCESS)) { + Boolean allow = (Boolean) call.argument("allow"); + serviceWorkerWebSettings.setAllowFileAccess(allow); + } + result.success(true); + break; + case "setBlockNetworkLoads": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS)) { + Boolean flag = (Boolean) call.argument("flag"); + serviceWorkerWebSettings.setBlockNetworkLoads(flag); + } + result.success(true); + break; + case "setCacheMode": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_CACHE_MODE)) { + Integer mode = (Integer) call.argument("mode"); + serviceWorkerWebSettings.setCacheMode(mode); + } + result.success(true); + break; + default: + result.notImplemented(); + } + } + + public static class ShouldInterceptRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return WebResourceResponseExt.fromMap((Map) obj); + } + } + + public void shouldInterceptRequest(WebResourceRequestExt request, @NonNull ShouldInterceptRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) return; + channel.invokeMethod("shouldInterceptRequest", request.toMap(), callback); + } + + public static class SyncShouldInterceptRequestCallback extends SyncBaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return (new ShouldInterceptRequestCallback()).decodeResult(obj); + } + } + + @Nullable + public WebResourceResponseExt shouldInterceptRequest(WebResourceRequestExt request) throws InterruptedException { + MethodChannel channel = getChannel(); + if (channel == null) return null; + final SyncShouldInterceptRequestCallback callback = new SyncShouldInterceptRequestCallback(); + return Util.invokeMethodAndWaitResult(channel, "shouldInterceptRequest", request.toMap(), callback); + } + + @Override + public void dispose() { + super.dispose(); + serviceWorkerManager = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerManager.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerManager.java new file mode 100644 index 0000000..4dde63e --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerManager.java @@ -0,0 +1,115 @@ +package com.pichillilorenzo.flutter_inappwebview_android.service_worker; + +import android.os.Build; +import android.util.Log; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.webkit.ServiceWorkerClientCompat; +import androidx.webkit.ServiceWorkerControllerCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceResponseExt; + +import java.io.ByteArrayInputStream; +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +@RequiresApi(api = Build.VERSION_CODES.N) +public class ServiceWorkerManager implements Disposable { + protected static final String LOG_TAG = "ServiceWorkerManager"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_serviceworkercontroller"; + + @Nullable + public ServiceWorkerChannelDelegate channelDelegate; + @Nullable + public static ServiceWorkerControllerCompat serviceWorkerController; + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public ServiceWorkerManager(@NonNull final InAppWebViewFlutterPlugin plugin) { + this.plugin = plugin; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME); + this.channelDelegate = new ServiceWorkerChannelDelegate(this, channel); + } + + public static void init() { + if (serviceWorkerController == null && + WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BASIC_USAGE)) { + serviceWorkerController = ServiceWorkerControllerCompat.getInstance(); + } + } + + public void setServiceWorkerClient(Boolean isNull) { + if (serviceWorkerController != null) { + // set ServiceWorkerClient as null makes the app crashes, so just set a dummy ServiceWorkerClientCompat. + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1151 + serviceWorkerController.setServiceWorkerClient(isNull ? dummyServiceWorkerClientCompat() : new ServiceWorkerClientCompat() { + @Nullable + @Override + public WebResourceResponse shouldInterceptRequest(@NonNull WebResourceRequest request) { + WebResourceRequestExt requestExt = WebResourceRequestExt.fromWebResourceRequest(request); + + WebResourceResponseExt response = null; + if (channelDelegate != null) { + try { + response = channelDelegate.shouldInterceptRequest(requestExt); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + } + + if (response != null) { + String contentType = response.getContentType(); + String contentEncoding = response.getContentEncoding(); + byte[] data = response.getData(); + Map responseHeaders = response.getHeaders(); + Integer statusCode = response.getStatusCode(); + String reasonPhrase = response.getReasonPhrase(); + + ByteArrayInputStream inputStream = (data != null) ? new ByteArrayInputStream(data) : null; + + if (statusCode != null && reasonPhrase != null) { + return new WebResourceResponse(contentType, contentEncoding, statusCode, reasonPhrase, responseHeaders, inputStream); + } else { + return new WebResourceResponse(contentType, contentEncoding, inputStream); + } + } + + return null; + } + }); + } + } + + private ServiceWorkerClientCompat dummyServiceWorkerClientCompat() { + return DummyServiceWorkerClientCompat.INSTANCE; + } + + @Override + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + plugin = null; + } + + private static final class DummyServiceWorkerClientCompat extends ServiceWorkerClientCompat { + static final ServiceWorkerClientCompat INSTANCE = new DummyServiceWorkerClientCompat(); + + @Nullable + @Override + public WebResourceResponse shouldInterceptRequest(@NonNull WebResourceRequest request) { + return null; + } + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerChannelDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerChannelDelegate.java new file mode 100644 index 0000000..e6b957a --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerChannelDelegate.java @@ -0,0 +1,78 @@ +package com.pichillilorenzo.flutter_inappwebview_android.tracing; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.TracingConfig; +import androidx.webkit.TracingController; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.util.Map; +import java.util.concurrent.Executors; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class TracingControllerChannelDelegate extends ChannelDelegateImpl { + @Nullable + private TracingControllerManager tracingControllerManager; + + public TracingControllerChannelDelegate(@NonNull TracingControllerManager tracingControllerManager, @NonNull MethodChannel channel) { + super(channel); + this.tracingControllerManager = tracingControllerManager; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + TracingControllerManager.init(); + TracingController tracingController = TracingControllerManager.tracingController; + + switch (call.method) { + case "isTracing": + if (tracingController != null) { + result.success(tracingController.isTracing()); + } else { + result.success(false); + } + break; + case "start": + if (tracingController != null && WebViewFeature.isFeatureSupported(WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE)) { + Map settingsMap = (Map) call.argument("settings"); + TracingSettings settings = new TracingSettings(); + settings.parse(settingsMap); + TracingConfig config = TracingControllerManager.buildTracingConfig(settings); + tracingController.start(config); + result.success(true); + } else { + result.success(false); + } + break; + case "stop": + if (tracingController != null && WebViewFeature.isFeatureSupported(WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE)) { + String filePath = (String) call.argument("filePath"); + try { + result.success(tracingController.stop( + filePath != null ? new FileOutputStream(filePath) : null, + Executors.newSingleThreadExecutor())); + } catch (FileNotFoundException e) { + e.printStackTrace(); + result.success(false); + } + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + @Override + public void dispose() { + super.dispose(); + tracingControllerManager = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerManager.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerManager.java new file mode 100644 index 0000000..6c48c5b --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerManager.java @@ -0,0 +1,61 @@ +package com.pichillilorenzo.flutter_inappwebview_android.tracing; + +import androidx.annotation.Nullable; +import androidx.webkit.TracingConfig; +import androidx.webkit.TracingController; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; + +import io.flutter.plugin.common.MethodChannel; + +public class TracingControllerManager implements Disposable { + protected static final String LOG_TAG = "TracingControllerMan"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_tracingcontroller"; + + @Nullable + public TracingControllerChannelDelegate channelDelegate; + @Nullable + public static TracingController tracingController; + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public TracingControllerManager(final InAppWebViewFlutterPlugin plugin) { + this.plugin = plugin; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME); + this.channelDelegate = new TracingControllerChannelDelegate(this, channel); + } + + public static void init() { + if (tracingController == null && + WebViewFeature.isFeatureSupported(WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE)) { + tracingController = TracingController.getInstance(); + } + } + + public static TracingConfig buildTracingConfig(TracingSettings settings) { + TracingConfig.Builder builder = new TracingConfig.Builder(); + for (Object category : settings.categories) { + if (category instanceof String) { + builder.addCategories((String) category); + } + if (category instanceof Integer) { + builder.addCategories((Integer) category); + } + } + if (settings.tracingMode != null) { + builder.setTracingMode(settings.tracingMode); + } + return builder.build(); + } + + @Override + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingSettings.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingSettings.java new file mode 100644 index 0000000..bcf0bd5 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingSettings.java @@ -0,0 +1,61 @@ +package com.pichillilorenzo.flutter_inappwebview_android.tracing; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.TracingController; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TracingSettings implements ISettings { + + public static final String LOG_TAG = "TracingSettings"; + + @NonNull + public List categories = new ArrayList<>(); + @Nullable + public Integer tracingMode; + + @NonNull + @Override + public TracingSettings parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "categories": + categories = (List) value; + break; + case "tracingMode": + tracingMode = (Integer) value; + break; + } + } + + return this; + } + + @NonNull + @Override + public Map toMap() { + Map settings = new HashMap<>(); + settings.put("categories", categories); + settings.put("tracingMode", tracingMode); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull TracingController tracingController) { + Map realSettings = toMap(); + return realSettings; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/AndroidResource.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/AndroidResource.java new file mode 100644 index 0000000..6db0d08 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/AndroidResource.java @@ -0,0 +1,103 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class AndroidResource { + @NonNull + private String name; + @Nullable + private String defType; + @Nullable + private String defPackage; + + public AndroidResource(@NonNull String name, @Nullable String defType, @Nullable String defPackage) { + this.name = name; + this.defType = defType; + this.defPackage = defPackage; + } + + @Nullable + public static AndroidResource fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String name = (String) map.get("name"); + String defType = (String) map.get("defType"); + String defPackage = (String) map.get("defPackage"); + return new AndroidResource(name, defType, defPackage); + } + + public Map toMap() { + Map urlRequestMap = new HashMap<>(); + urlRequestMap.put("name", name); + urlRequestMap.put("defType", defType); + urlRequestMap.put("defPackage", defPackage); + return urlRequestMap; + } + + @NonNull + public String getName() { + return name; + } + + public void setName(@NonNull String name) { + this.name = name; + } + + @Nullable + public String getDefType() { + return defType; + } + + public void setDefType(@Nullable String defType) { + this.defType = defType; + } + + @Nullable + public String getDefPackage() { + return defPackage; + } + + public void setDefPackage(@Nullable String defPackage) { + this.defPackage = defPackage; + } + + public int getIdentifier(@NonNull Context ctx) { + return ctx.getResources().getIdentifier(name, defType, defPackage); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AndroidResource that = (AndroidResource) o; + + if (!name.equals(that.name)) return false; + if (defType != null ? !defType.equals(that.defType) : that.defType != null) return false; + return defPackage != null ? defPackage.equals(that.defPackage) : that.defPackage == null; + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + (defType != null ? defType.hashCode() : 0); + result = 31 * result + (defPackage != null ? defPackage.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "AndroidResource{" + + "name='" + name + '\'' + + ", type='" + defType + '\'' + + ", defPackage='" + defPackage + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/BaseCallbackResultImpl.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/BaseCallbackResultImpl.java new file mode 100644 index 0000000..bd23e53 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/BaseCallbackResultImpl.java @@ -0,0 +1,47 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class BaseCallbackResultImpl implements ICallbackResult { + @Override + public boolean nonNullSuccess(@NonNull T result) { + return true; + } + + @Override + public boolean nullSuccess() { + return true; + } + + @Override + public void defaultBehaviour(@Nullable T result) {} + + @Override + public void success(@Nullable Object obj) { + T result = decodeResult(obj); + boolean shouldRunDefaultBehaviour; + if (result == null) { + shouldRunDefaultBehaviour = nullSuccess(); + } else { + shouldRunDefaultBehaviour = nonNullSuccess(result); + } + if (shouldRunDefaultBehaviour) { + defaultBehaviour(result); + } + } + + @Nullable + @Override + public T decodeResult(@Nullable Object obj) { + return null; + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {} + + @Override + public void notImplemented() { + defaultBehaviour(null); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ChannelDelegateImpl.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ChannelDelegateImpl.java new file mode 100644 index 0000000..450a61b --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ChannelDelegateImpl.java @@ -0,0 +1,37 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class ChannelDelegateImpl implements IChannelDelegate { + @Nullable + private MethodChannel channel; + + public ChannelDelegateImpl(@NonNull MethodChannel channel) { + this.channel = channel; + this.channel.setMethodCallHandler(this); + } + + @Override + @Nullable + public MethodChannel getChannel() { + return channel; + } + + @CallSuper + public void dispose() { + if (channel != null) { + channel.setMethodCallHandler(null); + channel = null; + } + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertChallenge.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertChallenge.java new file mode 100644 index 0000000..1bb5afe --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertChallenge.java @@ -0,0 +1,85 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class ClientCertChallenge extends URLAuthenticationChallenge { + @Nullable + private Principal[] principals; + @Nullable + private String[] keyTypes; + + public ClientCertChallenge(URLProtectionSpace protectionSpace, @Nullable Principal[] principals, @Nullable String[] keyTypes) { + super(protectionSpace); + this.principals = principals; + this.keyTypes = keyTypes; + } + + public Map toMap() { + List principalList = null; + if (principals != null) { + principalList = new ArrayList<>(); + for (Principal principal : principals) { + principalList.add(principal.getName()); + } + } + + Map challengeMap = super.toMap(); + challengeMap.put("principals", principalList); + challengeMap.put("keyTypes", keyTypes != null ? Arrays.asList(keyTypes) : null); + return challengeMap; + } + + @Nullable + public Principal[] getPrincipals() { + return principals; + } + + public void setPrincipals(@Nullable Principal[] principals) { + this.principals = principals; + } + + @Nullable + public String[] getKeyTypes() { + return keyTypes; + } + + public void setKeyTypes(@Nullable String[] keyTypes) { + this.keyTypes = keyTypes; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + ClientCertChallenge that = (ClientCertChallenge) o; + + // Probably incorrect - comparing Object[] arrays with Arrays.equals + if (!Arrays.equals(principals, that.principals)) return false; + // Probably incorrect - comparing Object[] arrays with Arrays.equals + return Arrays.equals(keyTypes, that.keyTypes); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Arrays.hashCode(principals); + result = 31 * result + Arrays.hashCode(keyTypes); + return result; + } + + @Override + public String toString() { + return "ClientCertChallenge{" + + "principals=" + Arrays.toString(principals) + + ", keyTypes=" + Arrays.toString(keyTypes) + + "} " + super.toString(); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertResponse.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertResponse.java new file mode 100644 index 0000000..cbd4349 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertResponse.java @@ -0,0 +1,105 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class ClientCertResponse { + @NonNull + private String certificatePath; + @Nullable + private String certificatePassword; + @NonNull + private String keyStoreType; + @Nullable + private Integer action; + + public ClientCertResponse(@NonNull String certificatePath, @Nullable String certificatePassword, @NonNull String keyStoreType, @Nullable Integer action) { + this.certificatePath = certificatePath; + this.certificatePassword = certificatePassword; + this.keyStoreType = keyStoreType; + this.action = action; + } + + @Nullable + public static ClientCertResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String certificatePath = (String) map.get("certificatePath"); + String certificatePassword = (String) map.get("certificatePassword"); + String keyStoreType = (String) map.get("keyStoreType"); + Integer action = (Integer) map.get("action"); + return new ClientCertResponse(certificatePath, certificatePassword, keyStoreType, action); + } + + @NonNull + public String getCertificatePath() { + return certificatePath; + } + + public void setCertificatePath(@NonNull String certificatePath) { + this.certificatePath = certificatePath; + } + + @Nullable + public String getCertificatePassword() { + return certificatePassword; + } + + public void setCertificatePassword(@Nullable String certificatePassword) { + this.certificatePassword = certificatePassword; + } + + @NonNull + public String getKeyStoreType() { + return keyStoreType; + } + + public void setKeyStoreType(@NonNull String keyStoreType) { + this.keyStoreType = keyStoreType; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClientCertResponse that = (ClientCertResponse) o; + + if (!certificatePath.equals(that.certificatePath)) return false; + if (certificatePassword != null ? !certificatePassword.equals(that.certificatePassword) : that.certificatePassword != null) + return false; + if (!keyStoreType.equals(that.keyStoreType)) return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = certificatePath.hashCode(); + result = 31 * result + (certificatePassword != null ? certificatePassword.hashCode() : 0); + result = 31 * result + keyStoreType.hashCode(); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ClientCertResponse{" + + "certificatePath='" + certificatePath + '\'' + + ", certificatePassword='" + certificatePassword + '\'' + + ", keyStoreType='" + keyStoreType + '\'' + + ", action=" + action + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ContentWorld.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ContentWorld.java new file mode 100644 index 0000000..8c1fd25 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ContentWorld.java @@ -0,0 +1,63 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class ContentWorld { + @NonNull + private String name; + + public static final ContentWorld PAGE = new ContentWorld("page"); + public static final ContentWorld DEFAULT_CLIENT = new ContentWorld("defaultClient"); + + private ContentWorld(@NonNull String name) { + this.name = name; + } + + public static ContentWorld world(@NonNull String name) { + return new ContentWorld(name); + } + + @Nullable + public static ContentWorld fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String name = (String) map.get("name"); + assert name != null; + return new ContentWorld(name); + } + + @NonNull + public String getName() { + return name; + } + + public void setName(@NonNull String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ContentWorld that = (ContentWorld) o; + + return name.equals(that.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return "ContentWorld{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CreateWindowAction.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CreateWindowAction.java new file mode 100644 index 0000000..d12b225 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CreateWindowAction.java @@ -0,0 +1,70 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import java.util.Map; + +public class CreateWindowAction extends NavigationAction { + int windowId; + boolean isDialog; + + public CreateWindowAction(URLRequest request, boolean isForMainFrame, boolean hasGesture, boolean isRedirect, int windowId, boolean isDialog) { + super(request, isForMainFrame, hasGesture, isRedirect); + this.windowId = windowId; + this.isDialog = isDialog; + } + + public Map toMap() { + Map createWindowActionMap = super.toMap(); + createWindowActionMap.put("windowId", windowId); + createWindowActionMap.put("isDialog", isDialog); + createWindowActionMap.put("windowFeatures", null); + return createWindowActionMap; + } + + public int getWindowId() { + return windowId; + } + + public void setWindowId(int windowId) { + this.windowId = windowId; + } + + public boolean isDialog() { + return isDialog; + } + + public void setDialog(boolean dialog) { + isDialog = dialog; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + CreateWindowAction that = (CreateWindowAction) o; + + if (windowId != that.windowId) return false; + return isDialog == that.isDialog; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + windowId; + result = 31 * result + (isDialog ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "CreateWindowAction{" + + "windowId=" + windowId + + ", isDialog=" + isDialog + + ", request=" + request + + ", isForMainFrame=" + isForMainFrame + + ", hasGesture=" + hasGesture + + ", isRedirect=" + isRedirect + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomSchemeResponse.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomSchemeResponse.java new file mode 100644 index 0000000..50bac10 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomSchemeResponse.java @@ -0,0 +1,89 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.Map; + +public class CustomSchemeResponse { + @NonNull + private byte[] data; + @NonNull + private String contentType; + @NonNull + private String contentEncoding; + + public CustomSchemeResponse(@NonNull byte[] data, @NonNull String contentType, @NonNull String contentEncoding) { + this.data = data; + this.contentType = contentType; + this.contentEncoding = contentEncoding; + } + + @Nullable + public static CustomSchemeResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + byte[] data = (byte[]) map.get("data"); + String contentType = (String) map.get("contentType"); + String contentEncoding = (String) map.get("contentEncoding"); + return new CustomSchemeResponse(data, contentType, contentEncoding); + } + + @NonNull + public byte[] getData() { + return data; + } + + public void setData(@NonNull byte[] data) { + this.data = data; + } + + @NonNull + public String getContentType() { + return contentType; + } + + public void setContentType(@NonNull String contentType) { + this.contentType = contentType; + } + + @NonNull + public String getContentEncoding() { + return contentEncoding; + } + + public void setContentEncoding(@NonNull String contentEncoding) { + this.contentEncoding = contentEncoding; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CustomSchemeResponse that = (CustomSchemeResponse) o; + + if (!Arrays.equals(data, that.data)) return false; + if (!contentType.equals(that.contentType)) return false; + return contentEncoding.equals(that.contentEncoding); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(data); + result = 31 * result + contentType.hashCode(); + result = 31 * result + contentEncoding.hashCode(); + return result; + } + + @Override + public String toString() { + return "CustomSchemeResponse{" + + "data=" + Arrays.toString(data) + + ", contentType='" + contentType + '\'' + + ", contentEncoding='" + contentEncoding + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsActionButton.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsActionButton.java new file mode 100644 index 0000000..b408d32 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsActionButton.java @@ -0,0 +1,104 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.Map; + +public class CustomTabsActionButton { + private int id; + + @NonNull + private byte[] icon; + + @NonNull + private String description; + + private boolean shouldTint; + + public CustomTabsActionButton(int id, @NonNull byte[] icon, @NonNull String description, boolean shouldTint) { + this.id = id; + this.icon = icon; + this.description = description; + this.shouldTint = shouldTint; + } + + @Nullable + public static CustomTabsActionButton fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + int id = (int) map.get("id"); + byte[] icon = (byte[]) map.get("icon"); + String description = (String) map.get("description"); + boolean shouldTint = (boolean) map.get("shouldTint"); + return new CustomTabsActionButton(id, icon, description, shouldTint); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @NonNull + public byte[] getIcon() { + return icon; + } + + public void setIcon(@NonNull byte[] icon) { + this.icon = icon; + } + + @NonNull + public String getDescription() { + return description; + } + + public void setDescription(@NonNull String description) { + this.description = description; + } + + public boolean isShouldTint() { + return shouldTint; + } + + public void setShouldTint(boolean shouldTint) { + this.shouldTint = shouldTint; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CustomTabsActionButton that = (CustomTabsActionButton) o; + + if (id != that.id) return false; + if (shouldTint != that.shouldTint) return false; + if (!Arrays.equals(icon, that.icon)) return false; + return description.equals(that.description); + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + Arrays.hashCode(icon); + result = 31 * result + description.hashCode(); + result = 31 * result + (shouldTint ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "CustomTabsActionButton{" + + "id=" + id + + ", icon=" + Arrays.toString(icon) + + ", description='" + description + '\'' + + ", shouldTint=" + shouldTint + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsMenuItem.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsMenuItem.java new file mode 100644 index 0000000..34cf997 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsMenuItem.java @@ -0,0 +1,71 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class CustomTabsMenuItem { + private int id; + + @NonNull + private String label; + + public CustomTabsMenuItem(int id, @NonNull String label) { + this.id = id; + this.label = label; + } + + @Nullable + public static CustomTabsMenuItem fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + int id = (int) map.get("id"); + String label = (String) map.get("label"); + return new CustomTabsMenuItem(id, label); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @NonNull + public String getLabel() { + return label; + } + + public void setLabel(@NonNull String label) { + this.label = label; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CustomTabsMenuItem that = (CustomTabsMenuItem) o; + + if (id != that.id) return false; + return label.equals(that.label); + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + label.hashCode(); + return result; + } + + @Override + public String toString() { + return "CustomTabsMenuItem{" + + "id=" + id + + ", label='" + label + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsSecondaryToolbar.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsSecondaryToolbar.java new file mode 100644 index 0000000..7db298e --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsSecondaryToolbar.java @@ -0,0 +1,83 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CustomTabsSecondaryToolbar { + @NonNull + private AndroidResource layout; + @NonNull + private List clickableIDs = new ArrayList<>(); + + public CustomTabsSecondaryToolbar(@NonNull AndroidResource layout, @NonNull List clickableIDs) { + this.layout = layout; + this.clickableIDs = clickableIDs; + } + + @Nullable + public static CustomTabsSecondaryToolbar fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + AndroidResource layout = AndroidResource.fromMap((Map) map.get("layout")); + List clickableIDs = new ArrayList<>(); + List> clickableIDList = (List>) map.get("clickableIDs"); + if (clickableIDList != null) { + for (Map clickableIDMap : clickableIDList) { + AndroidResource clickableID = AndroidResource.fromMap((Map) clickableIDMap.get("id")); + if (clickableID != null) { + clickableIDs.add(clickableID); + } + } + } + return new CustomTabsSecondaryToolbar(layout, clickableIDs); + } + + @NonNull + public AndroidResource getLayout() { + return layout; + } + + public void setLayout(@NonNull AndroidResource layout) { + this.layout = layout; + } + + @NonNull + public List getClickableIDs() { + return clickableIDs; + } + + public void setClickableIDs(@NonNull List clickableIDs) { + this.clickableIDs = clickableIDs; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CustomTabsSecondaryToolbar that = (CustomTabsSecondaryToolbar) o; + + if (!layout.equals(that.layout)) return false; + return clickableIDs.equals(that.clickableIDs); + } + + @Override + public int hashCode() { + int result = layout.hashCode(); + result = 31 * result + clickableIDs.hashCode(); + return result; + } + + @Override + public String toString() { + return "CustomTabsSecondaryToolbar{" + + "layout=" + layout + + ", clickableIDs=" + clickableIDs + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Disposable.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Disposable.java new file mode 100644 index 0000000..0cd54c0 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Disposable.java @@ -0,0 +1,5 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +public interface Disposable { + void dispose(); +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/DownloadStartRequest.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/DownloadStartRequest.java new file mode 100644 index 0000000..3dd8d3b --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/DownloadStartRequest.java @@ -0,0 +1,150 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class DownloadStartRequest { + + @NonNull + private String url; + @NonNull + private String userAgent; + @NonNull + private String contentDisposition; + @NonNull + private String mimeType; + private long contentLength; + @Nullable + private String suggestedFilename; + @Nullable + private String textEncodingName; + + public DownloadStartRequest(@NonNull String url, @NonNull String userAgent, @NonNull String contentDisposition, @NonNull String mimeType, long contentLength, @Nullable String suggestedFilename, @Nullable String textEncodingName) { + this.url = url; + this.userAgent = userAgent; + this.contentDisposition = contentDisposition; + this.mimeType = mimeType; + this.contentLength = contentLength; + this.suggestedFilename = suggestedFilename; + this.textEncodingName = textEncodingName; + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("userAgent", userAgent); + obj.put("contentDisposition", contentDisposition); + obj.put("mimeType", mimeType); + obj.put("contentLength", contentLength); + obj.put("suggestedFilename", suggestedFilename); + obj.put("textEncodingName", textEncodingName); + return obj; + } + + @NonNull + public String getUrl() { + return url; + } + + public void setUrl(@NonNull String url) { + this.url = url; + } + + @NonNull + public String getUserAgent() { + return userAgent; + } + + public void setUserAgent(@NonNull String userAgent) { + this.userAgent = userAgent; + } + + @NonNull + public String getContentDisposition() { + return contentDisposition; + } + + public void setContentDisposition(@NonNull String contentDisposition) { + this.contentDisposition = contentDisposition; + } + + @NonNull + public String getMimeType() { + return mimeType; + } + + public void setMimeType(@NonNull String mimeType) { + this.mimeType = mimeType; + } + + public long getContentLength() { + return contentLength; + } + + public void setContentLength(long contentLength) { + this.contentLength = contentLength; + } + + @Nullable + public String getSuggestedFilename() { + return suggestedFilename; + } + + public void setSuggestedFilename(@Nullable String suggestedFilename) { + this.suggestedFilename = suggestedFilename; + } + + @Nullable + public String getTextEncodingName() { + return textEncodingName; + } + + public void setTextEncodingName(@Nullable String textEncodingName) { + this.textEncodingName = textEncodingName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DownloadStartRequest that = (DownloadStartRequest) o; + + if (contentLength != that.contentLength) return false; + if (!url.equals(that.url)) return false; + if (!userAgent.equals(that.userAgent)) return false; + if (!contentDisposition.equals(that.contentDisposition)) return false; + if (!mimeType.equals(that.mimeType)) return false; + if (suggestedFilename != null ? !suggestedFilename.equals(that.suggestedFilename) : that.suggestedFilename != null) + return false; + return textEncodingName != null ? textEncodingName.equals(that.textEncodingName) : that.textEncodingName == null; + } + + @Override + public int hashCode() { + int result = url.hashCode(); + result = 31 * result + userAgent.hashCode(); + result = 31 * result + contentDisposition.hashCode(); + result = 31 * result + mimeType.hashCode(); + result = 31 * result + (int) (contentLength ^ (contentLength >>> 32)); + result = 31 * result + (suggestedFilename != null ? suggestedFilename.hashCode() : 0); + result = 31 * result + (textEncodingName != null ? textEncodingName.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "DownloadStartRequest{" + + "url='" + url + '\'' + + ", userAgent='" + userAgent + '\'' + + ", contentDisposition='" + contentDisposition + '\'' + + ", mimeType='" + mimeType + '\'' + + ", contentLength=" + contentLength + + ", suggestedFilename='" + suggestedFilename + '\'' + + ", textEncodingName='" + textEncodingName + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/FindSession.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/FindSession.java new file mode 100644 index 0000000..a4df7d7 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/FindSession.java @@ -0,0 +1,76 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import java.util.HashMap; +import java.util.Map; + +public class FindSession { + private int resultCount; + private int highlightedResultIndex; + private int searchResultDisplayStyle = 2; // matches NONE of iOS + + public FindSession(int resultCount, int highlightedResultIndex) { + this.resultCount = resultCount; + this.highlightedResultIndex = highlightedResultIndex; + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("resultCount", resultCount); + obj.put("highlightedResultIndex", highlightedResultIndex); + obj.put("searchResultDisplayStyle", searchResultDisplayStyle); + return obj; + } + + public int getResultCount() { + return resultCount; + } + + public void setResultCount(int resultCount) { + this.resultCount = resultCount; + } + + public int getHighlightedResultIndex() { + return highlightedResultIndex; + } + + public void setHighlightedResultIndex(int highlightedResultIndex) { + this.highlightedResultIndex = highlightedResultIndex; + } + + public int getSearchResultDisplayStyle() { + return searchResultDisplayStyle; + } + + public void setSearchResultDisplayStyle(int searchResultDisplayStyle) { + this.searchResultDisplayStyle = searchResultDisplayStyle; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FindSession that = (FindSession) o; + + if (resultCount != that.resultCount) return false; + if (highlightedResultIndex != that.highlightedResultIndex) return false; + return searchResultDisplayStyle == that.searchResultDisplayStyle; + } + + @Override + public int hashCode() { + int result = resultCount; + result = 31 * result + highlightedResultIndex; + result = 31 * result + searchResultDisplayStyle; + return result; + } + + @Override + public String toString() { + return "FindSession{" + + "resultCount=" + resultCount + + ", highlightedResultIndex=" + highlightedResultIndex + + ", searchResultDisplayStyle=" + searchResultDisplayStyle + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/GeolocationPermissionShowPromptResponse.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/GeolocationPermissionShowPromptResponse.java new file mode 100644 index 0000000..14340e9 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/GeolocationPermissionShowPromptResponse.java @@ -0,0 +1,84 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class GeolocationPermissionShowPromptResponse { + @NonNull + private String origin; + boolean allow; + boolean retain; + + public GeolocationPermissionShowPromptResponse(@NonNull String origin, boolean allow, boolean retain) { + this.origin = origin; + this.allow = allow; + this.retain = retain; + } + + @Nullable + public static GeolocationPermissionShowPromptResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String origin = (String) map.get("origin"); + boolean allow = (boolean) map.get("allow"); + boolean retain = (boolean) map.get("retain"); + return new GeolocationPermissionShowPromptResponse(origin, allow, retain); + } + + @NonNull + public String getOrigin() { + return origin; + } + + public void setOrigin(@NonNull String origin) { + this.origin = origin; + } + + public boolean isAllow() { + return allow; + } + + public void setAllow(boolean allow) { + this.allow = allow; + } + + public boolean isRetain() { + return retain; + } + + public void setRetain(boolean retain) { + this.retain = retain; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GeolocationPermissionShowPromptResponse that = (GeolocationPermissionShowPromptResponse) o; + + if (allow != that.allow) return false; + if (retain != that.retain) return false; + return origin.equals(that.origin); + } + + @Override + public int hashCode() { + int result = origin.hashCode(); + result = 31 * result + (allow ? 1 : 0); + result = 31 * result + (retain ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "GeolocationPermissionShowPromptResponse{" + + "origin='" + origin + '\'' + + ", allow=" + allow + + ", retain=" + retain + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HitTestResult.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HitTestResult.java new file mode 100644 index 0000000..87615fa --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HitTestResult.java @@ -0,0 +1,79 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.webkit.WebView; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class HitTestResult { + private int type; + @Nullable + private String extra; + + public HitTestResult(int type, @Nullable String extra) { + this.type = type; + this.extra = extra; + } + + @Nullable + static public HitTestResult fromWebViewHitTestResult(@Nullable WebView.HitTestResult hitTestResult) { + if (hitTestResult == null) { + return null; + } + + return new HitTestResult(hitTestResult.getType(), hitTestResult.getExtra()); + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + @Nullable + public String getExtra() { + return extra; + } + + public void setExtra(@Nullable String extra) { + this.extra = extra; + } + + @Nullable + public Map toMap() { + Map hitTestResultMap = new HashMap<>(); + hitTestResultMap.put("type", type); + hitTestResultMap.put("extra", extra); + return hitTestResultMap; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HitTestResult that = (HitTestResult) o; + + if (type != that.type) return false; + return extra != null ? extra.equals(that.extra) : that.extra == null; + } + + @Override + public int hashCode() { + int result = type; + result = 31 * result + (extra != null ? extra.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "HitTestResultMap{" + + "type=" + type + + ", extra='" + extra + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthResponse.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthResponse.java new file mode 100644 index 0000000..123a1b0 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthResponse.java @@ -0,0 +1,102 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class HttpAuthResponse { + @NonNull + private String username; + @NonNull + private String password; + boolean permanentPersistence; + @Nullable + private Integer action; + + public HttpAuthResponse(@NonNull String username, @NonNull String password, boolean permanentPersistence, @Nullable Integer action) { + this.username = username; + this.password = password; + this.permanentPersistence = permanentPersistence; + this.action = action; + } + + @Nullable + public static HttpAuthResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String username = (String) map.get("username"); + String password = (String) map.get("password"); + boolean permanentPersistence = (boolean) map.get("permanentPersistence"); + Integer action = (Integer) map.get("action"); + return new HttpAuthResponse(username, password, permanentPersistence, action); + } + + @NonNull + public String getUsername() { + return username; + } + + public void setUsername(@NonNull String username) { + this.username = username; + } + + @NonNull + public String getPassword() { + return password; + } + + public void setPassword(@NonNull String password) { + this.password = password; + } + + public boolean isPermanentPersistence() { + return permanentPersistence; + } + + public void setPermanentPersistence(boolean permanentPersistence) { + this.permanentPersistence = permanentPersistence; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HttpAuthResponse that = (HttpAuthResponse) o; + + if (permanentPersistence != that.permanentPersistence) return false; + if (!username.equals(that.username)) return false; + if (!password.equals(that.password)) return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = username.hashCode(); + result = 31 * result + password.hashCode(); + result = 31 * result + (permanentPersistence ? 1 : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "HttpAuthResponse{" + + "username='" + username + '\'' + + ", password='" + password + '\'' + + ", permanentPersistence=" + permanentPersistence + + ", action=" + action + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthenticationChallenge.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthenticationChallenge.java new file mode 100644 index 0000000..66edfa4 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthenticationChallenge.java @@ -0,0 +1,71 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class HttpAuthenticationChallenge extends URLAuthenticationChallenge { + private int previousFailureCount; + @Nullable + URLCredential proposedCredential; + + public HttpAuthenticationChallenge(URLProtectionSpace protectionSpace, int previousFailureCount, @Nullable URLCredential proposedCredential) { + super(protectionSpace); + this.previousFailureCount = previousFailureCount; + this.proposedCredential = proposedCredential; + } + + public Map toMap() { + Map challengeMap = super.toMap(); + challengeMap.put("previousFailureCount", previousFailureCount); + challengeMap.put("proposedCredential", (proposedCredential != null) ? proposedCredential.toMap() : null); + challengeMap.put("failureResponse", null); + challengeMap.put("error", null); + return challengeMap; + } + + public int getPreviousFailureCount() { + return previousFailureCount; + } + + public void setPreviousFailureCount(int previousFailureCount) { + this.previousFailureCount = previousFailureCount; + } + + @Nullable + public URLCredential getProposedCredential() { + return proposedCredential; + } + + public void setProposedCredential(@Nullable URLCredential proposedCredential) { + this.proposedCredential = proposedCredential; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + HttpAuthenticationChallenge that = (HttpAuthenticationChallenge) o; + + if (previousFailureCount != that.previousFailureCount) return false; + return proposedCredential != null ? proposedCredential.equals(that.proposedCredential) : that.proposedCredential == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + previousFailureCount; + result = 31 * result + (proposedCredential != null ? proposedCredential.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "HttpAuthenticationChallenge{" + + "previousFailureCount=" + previousFailureCount + + ", proposedCredential=" + proposedCredential + + "} " + super.toString(); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ICallbackResult.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ICallbackResult.java new file mode 100644 index 0000000..02515fc --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ICallbackResult.java @@ -0,0 +1,13 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.flutter.plugin.common.MethodChannel; + +public interface ICallbackResult extends MethodChannel.Result { + boolean nonNullSuccess(@NonNull T result); + boolean nullSuccess(); + void defaultBehaviour(@Nullable T result); + @Nullable T decodeResult(@Nullable Object obj); +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/IChannelDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/IChannelDelegate.java new file mode 100644 index 0000000..4d1ca8f --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/IChannelDelegate.java @@ -0,0 +1,10 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import io.flutter.plugin.common.MethodChannel; + +public interface IChannelDelegate extends MethodChannel.MethodCallHandler, Disposable { + @Nullable + MethodChannel getChannel(); +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/InAppBrowserMenuItem.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/InAppBrowserMenuItem.java new file mode 100644 index 0000000..85bba6e --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/InAppBrowserMenuItem.java @@ -0,0 +1,146 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; + +import java.util.Map; +import java.util.Objects; + +public class InAppBrowserMenuItem { + private int id; + + @NonNull + private String title; + + @Nullable + private Integer order; + + @Nullable + private Object icon; + + @Nullable + private String iconColor; + + private boolean showAsAction; + + public InAppBrowserMenuItem(int id, @NonNull String title, @Nullable Integer order, @Nullable Object icon, + @Nullable String iconColor, boolean showAsAction) { + this.id = id; + this.title = title; + this.order = order; + this.icon = icon; + this.iconColor = iconColor; + this.showAsAction = showAsAction; + } + + @Nullable + public static InAppBrowserMenuItem fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + int id = (int) map.get("id"); + String title = (String) map.get("title"); + Integer order = (Integer) map.get("order"); + Object icon = map.get("icon"); + if (icon instanceof Map) { + icon = AndroidResource.fromMap((Map) map.get("icon")); + } else if (!(icon instanceof byte[])) { + icon = null; + } + String iconColor = (String) map.get("iconColor"); + boolean showAsAction = Util.getOrDefault( map, "showAsAction", false); + return new InAppBrowserMenuItem(id, title, order, icon, iconColor, showAsAction); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @NonNull + public String getTitle() { + return title; + } + + public void setTitle(@NonNull String title) { + this.title = title; + } + + @Nullable + public Integer getOrder() { + return order; + } + + public void setOrder(@Nullable Integer order) { + this.order = order; + } + + @Nullable + public Object getIcon() { + return icon; + } + + public void setIcon(@Nullable Object icon) { + this.icon = icon; + } + + @Nullable + public String getIconColor() { + return iconColor; + } + + public void setIconColor(@Nullable String iconColor) { + this.iconColor = iconColor; + } + + public boolean isShowAsAction() { + return showAsAction; + } + + public void setShowAsAction(boolean showAsAction) { + this.showAsAction = showAsAction; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + InAppBrowserMenuItem that = (InAppBrowserMenuItem) o; + + if (id != that.id) return false; + if (showAsAction != that.showAsAction) return false; + if (!title.equals(that.title)) return false; + if (!Objects.equals(order, that.order)) return false; + if (!Objects.equals(icon, that.icon)) return false; + return Objects.equals(iconColor, that.iconColor); + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + title.hashCode(); + result = 31 * result + (order != null ? order.hashCode() : 0); + result = 31 * result + (icon != null ? icon.hashCode() : 0); + result = 31 * result + (iconColor != null ? iconColor.hashCode() : 0); + result = 31 * result + (showAsAction ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "InAppBrowserMenuItem{" + + "id=" + id + + ", title='" + title + '\'' + + ", order=" + order + + ", icon=" + icon + + ", iconColor='" + iconColor + '\'' + + ", showAsAction=" + showAsAction + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsAlertResponse.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsAlertResponse.java new file mode 100644 index 0000000..6f3f092 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsAlertResponse.java @@ -0,0 +1,98 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class JsAlertResponse { + private String message; + private String confirmButtonTitle; + private boolean handledByClient; + @Nullable + private Integer action; + + public JsAlertResponse(String message, String confirmButtonTitle, boolean handledByClient, @Nullable Integer action) { + this.message = message; + this.confirmButtonTitle = confirmButtonTitle; + this.handledByClient = handledByClient; + this.action = action; + } + + @Nullable + public static JsAlertResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String message = (String) map.get("message"); + String confirmButtonTitle = (String) map.get("confirmButtonTitle"); + boolean handledByClient = (boolean) map.get("handledByClient"); + Integer action = (Integer) map.get("action"); + return new JsAlertResponse(message, confirmButtonTitle, handledByClient, action); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getConfirmButtonTitle() { + return confirmButtonTitle; + } + + public void setConfirmButtonTitle(String confirmButtonTitle) { + this.confirmButtonTitle = confirmButtonTitle; + } + + public boolean isHandledByClient() { + return handledByClient; + } + + public void setHandledByClient(boolean handledByClient) { + this.handledByClient = handledByClient; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsAlertResponse that = (JsAlertResponse) o; + + if (handledByClient != that.handledByClient) return false; + if (message != null ? !message.equals(that.message) : that.message != null) return false; + if (confirmButtonTitle != null ? !confirmButtonTitle.equals(that.confirmButtonTitle) : that.confirmButtonTitle != null) + return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = message != null ? message.hashCode() : 0; + result = 31 * result + (confirmButtonTitle != null ? confirmButtonTitle.hashCode() : 0); + result = 31 * result + (handledByClient ? 1 : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "JsAlertResponse{" + + "message='" + message + '\'' + + ", confirmButtonTitle='" + confirmButtonTitle + '\'' + + ", handledByClient=" + handledByClient + + ", action=" + action + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsBeforeUnloadResponse.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsBeforeUnloadResponse.java new file mode 100644 index 0000000..ead00e0 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsBeforeUnloadResponse.java @@ -0,0 +1,113 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class JsBeforeUnloadResponse { + private String message; + private String confirmButtonTitle; + private String cancelButtonTitle; + private boolean handledByClient; + @Nullable + private Integer action; + + public JsBeforeUnloadResponse(String message, String confirmButtonTitle, String cancelButtonTitle, boolean handledByClient, @Nullable Integer action) { + this.message = message; + this.confirmButtonTitle = confirmButtonTitle; + this.cancelButtonTitle = cancelButtonTitle; + this.handledByClient = handledByClient; + this.action = action; + } + + @Nullable + public static JsBeforeUnloadResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String message = (String) map.get("message"); + String confirmButtonTitle = (String) map.get("confirmButtonTitle"); + String cancelButtonTitle = (String) map.get("cancelButtonTitle"); + boolean handledByClient = (boolean) map.get("handledByClient"); + Integer action = (Integer) map.get("action"); + return new JsBeforeUnloadResponse(message, confirmButtonTitle, cancelButtonTitle, handledByClient, action); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getConfirmButtonTitle() { + return confirmButtonTitle; + } + + public void setConfirmButtonTitle(String confirmButtonTitle) { + this.confirmButtonTitle = confirmButtonTitle; + } + + public String getCancelButtonTitle() { + return cancelButtonTitle; + } + + public void setCancelButtonTitle(String cancelButtonTitle) { + this.cancelButtonTitle = cancelButtonTitle; + } + + public boolean isHandledByClient() { + return handledByClient; + } + + public void setHandledByClient(boolean handledByClient) { + this.handledByClient = handledByClient; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsBeforeUnloadResponse that = (JsBeforeUnloadResponse) o; + + if (handledByClient != that.handledByClient) return false; + if (message != null ? !message.equals(that.message) : that.message != null) return false; + if (confirmButtonTitle != null ? !confirmButtonTitle.equals(that.confirmButtonTitle) : that.confirmButtonTitle != null) + return false; + if (cancelButtonTitle != null ? !cancelButtonTitle.equals(that.cancelButtonTitle) : that.cancelButtonTitle != null) + return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = message != null ? message.hashCode() : 0; + result = 31 * result + (confirmButtonTitle != null ? confirmButtonTitle.hashCode() : 0); + result = 31 * result + (cancelButtonTitle != null ? cancelButtonTitle.hashCode() : 0); + result = 31 * result + (handledByClient ? 1 : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "JsConfirmResponse{" + + "message='" + message + '\'' + + ", confirmButtonTitle='" + confirmButtonTitle + '\'' + + ", cancelButtonTitle='" + cancelButtonTitle + '\'' + + ", handledByClient=" + handledByClient + + ", action=" + action + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsConfirmResponse.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsConfirmResponse.java new file mode 100644 index 0000000..af65e41 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsConfirmResponse.java @@ -0,0 +1,113 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class JsConfirmResponse { + private String message; + private String confirmButtonTitle; + private String cancelButtonTitle; + private boolean handledByClient; + @Nullable + private Integer action; + + public JsConfirmResponse(String message, String confirmButtonTitle, String cancelButtonTitle, boolean handledByClient, @Nullable Integer action) { + this.message = message; + this.confirmButtonTitle = confirmButtonTitle; + this.cancelButtonTitle = cancelButtonTitle; + this.handledByClient = handledByClient; + this.action = action; + } + + @Nullable + public static JsConfirmResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String message = (String) map.get("message"); + String confirmButtonTitle = (String) map.get("confirmButtonTitle"); + String cancelButtonTitle = (String) map.get("cancelButtonTitle"); + boolean handledByClient = (boolean) map.get("handledByClient"); + Integer action = (Integer) map.get("action"); + return new JsConfirmResponse(message, confirmButtonTitle, cancelButtonTitle, handledByClient, action); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getConfirmButtonTitle() { + return confirmButtonTitle; + } + + public void setConfirmButtonTitle(String confirmButtonTitle) { + this.confirmButtonTitle = confirmButtonTitle; + } + + public String getCancelButtonTitle() { + return cancelButtonTitle; + } + + public void setCancelButtonTitle(String cancelButtonTitle) { + this.cancelButtonTitle = cancelButtonTitle; + } + + public boolean isHandledByClient() { + return handledByClient; + } + + public void setHandledByClient(boolean handledByClient) { + this.handledByClient = handledByClient; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsConfirmResponse that = (JsConfirmResponse) o; + + if (handledByClient != that.handledByClient) return false; + if (message != null ? !message.equals(that.message) : that.message != null) return false; + if (confirmButtonTitle != null ? !confirmButtonTitle.equals(that.confirmButtonTitle) : that.confirmButtonTitle != null) + return false; + if (cancelButtonTitle != null ? !cancelButtonTitle.equals(that.cancelButtonTitle) : that.cancelButtonTitle != null) + return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = message != null ? message.hashCode() : 0; + result = 31 * result + (confirmButtonTitle != null ? confirmButtonTitle.hashCode() : 0); + result = 31 * result + (cancelButtonTitle != null ? cancelButtonTitle.hashCode() : 0); + result = 31 * result + (handledByClient ? 1 : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "JsConfirmResponse{" + + "message='" + message + '\'' + + ", confirmButtonTitle='" + confirmButtonTitle + '\'' + + ", cancelButtonTitle='" + cancelButtonTitle + '\'' + + ", handledByClient=" + handledByClient + + ", action=" + action + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsPromptResponse.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsPromptResponse.java new file mode 100644 index 0000000..fb63aef --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsPromptResponse.java @@ -0,0 +1,145 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class JsPromptResponse { + private String message; + private String defaultValue; + private String confirmButtonTitle; + private String cancelButtonTitle; + private boolean handledByClient; + @Nullable + private String value; + @Nullable + private Integer action; + + public JsPromptResponse(String message, String defaultValue, String confirmButtonTitle, + String cancelButtonTitle, boolean handledByClient, @Nullable String value, @Nullable Integer action) { + this.message = message; + this.defaultValue = defaultValue; + this.confirmButtonTitle = confirmButtonTitle; + this.cancelButtonTitle = cancelButtonTitle; + this.handledByClient = handledByClient; + this.value = value; + this.action = action; + } + + @Nullable + public static JsPromptResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String message = (String) map.get("message"); + String defaultValue = (String) map.get("defaultValue"); + String confirmButtonTitle = (String) map.get("confirmButtonTitle"); + String cancelButtonTitle = (String) map.get("cancelButtonTitle"); + boolean handledByClient = (boolean) map.get("handledByClient"); + String value = (String) map.get("value"); + Integer action = (Integer) map.get("action"); + return new JsPromptResponse(message, defaultValue, confirmButtonTitle, cancelButtonTitle, handledByClient, value, action); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + public String getConfirmButtonTitle() { + return confirmButtonTitle; + } + + public void setConfirmButtonTitle(String confirmButtonTitle) { + this.confirmButtonTitle = confirmButtonTitle; + } + + public String getCancelButtonTitle() { + return cancelButtonTitle; + } + + public void setCancelButtonTitle(String cancelButtonTitle) { + this.cancelButtonTitle = cancelButtonTitle; + } + + public boolean isHandledByClient() { + return handledByClient; + } + + public void setHandledByClient(boolean handledByClient) { + this.handledByClient = handledByClient; + } + + @Nullable + public String getValue() { + return value; + } + + public void setValue(@Nullable String value) { + this.value = value; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsPromptResponse that = (JsPromptResponse) o; + + if (handledByClient != that.handledByClient) return false; + if (message != null ? !message.equals(that.message) : that.message != null) return false; + if (defaultValue != null ? !defaultValue.equals(that.defaultValue) : that.defaultValue != null) + return false; + if (confirmButtonTitle != null ? !confirmButtonTitle.equals(that.confirmButtonTitle) : that.confirmButtonTitle != null) + return false; + if (cancelButtonTitle != null ? !cancelButtonTitle.equals(that.cancelButtonTitle) : that.cancelButtonTitle != null) + return false; + if (value != null ? !value.equals(that.value) : that.value != null) return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = message != null ? message.hashCode() : 0; + result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0); + result = 31 * result + (confirmButtonTitle != null ? confirmButtonTitle.hashCode() : 0); + result = 31 * result + (cancelButtonTitle != null ? cancelButtonTitle.hashCode() : 0); + result = 31 * result + (handledByClient ? 1 : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "JsPromptResponse{" + + "message='" + message + '\'' + + ", defaultValue='" + defaultValue + '\'' + + ", confirmButtonTitle='" + confirmButtonTitle + '\'' + + ", cancelButtonTitle='" + cancelButtonTitle + '\'' + + ", handledByClient=" + handledByClient + + ", value='" + value + '\'' + + ", action=" + action + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MarginsExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MarginsExt.java new file mode 100644 index 0000000..161254d --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MarginsExt.java @@ -0,0 +1,150 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.print.PrintAttributes; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.HashMap; +import java.util.Map; + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class MarginsExt { + private double top; + private double right; + private double bottom; + private double left; + + public MarginsExt() {} + + public MarginsExt(double top, double right, double bottom, double left) { + this.top = top; + this.right = right; + this.bottom = bottom; + this.left = left; + } + + @Nullable + public static MarginsExt fromMargins(@Nullable PrintAttributes.Margins margins) { + if (margins == null) { + return null; + } + MarginsExt marginsExt = new MarginsExt(); + marginsExt.top = milsToPixels(margins.getTopMils()); + marginsExt.right = milsToPixels(margins.getRightMils()); + marginsExt.bottom = milsToPixels(margins.getBottomMils()); + marginsExt.left = milsToPixels(margins.getLeftMils()); + return marginsExt; + } + + @Nullable + public static MarginsExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + return new MarginsExt( + (double) map.get("top"), + (double) map.get("right"), + (double) map.get("bottom"), + (double) map.get("left")); + } + + public PrintAttributes.Margins toMargins() { + return new PrintAttributes.Margins( + pixelsToMils(left), + pixelsToMils(top), + pixelsToMils(right), + pixelsToMils(bottom) + ); + } + + // from mils to pixels + private static double milsToPixels(int mils) { + return mils * 0.09600001209449; + } + + // from pixels to mils + private static int pixelsToMils(double pixels) { + return (int) Math.round(pixels * 10.416665354331); + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("top", top); + obj.put("right", right); + obj.put("bottom", bottom); + obj.put("left", left); + return obj; + } + + public double getTop() { + return top; + } + + public void setTop(double top) { + this.top = top; + } + + public double getRight() { + return right; + } + + public void setRight(double right) { + this.right = right; + } + + public double getBottom() { + return bottom; + } + + public void setBottom(double bottom) { + this.bottom = bottom; + } + + public double getLeft() { + return left; + } + + public void setLeft(double left) { + this.left = left; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MarginsExt that = (MarginsExt) o; + + if (Double.compare(that.top, top) != 0) return false; + if (Double.compare(that.right, right) != 0) return false; + if (Double.compare(that.bottom, bottom) != 0) return false; + return Double.compare(that.left, left) == 0; + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(top); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(right); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(bottom); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(left); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "MarginsExt{" + + "top=" + top + + ", right=" + right + + ", bottom=" + bottom + + ", left=" + left + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MediaSizeExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MediaSizeExt.java new file mode 100644 index 0000000..cd2618f --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MediaSizeExt.java @@ -0,0 +1,134 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.print.PrintAttributes; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.HashMap; +import java.util.Map; + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class MediaSizeExt { + @NonNull + private String id; + @Nullable + private String label; + private int widthMils; + private int heightMils; + + public MediaSizeExt(@NonNull String id, @Nullable String label, int widthMils, int heightMils) { + this.id = id; + this.label = label; + this.widthMils = widthMils; + this.heightMils = heightMils; + } + + @Nullable + public static MediaSizeExt fromMediaSize(@Nullable PrintAttributes.MediaSize mediaSize) { + if (mediaSize == null) { + return null; + } + return new MediaSizeExt( + mediaSize.getId(), + null, + mediaSize.getHeightMils(), + mediaSize.getWidthMils() + ); + } + + @Nullable + public static MediaSizeExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String id = (String) map.get("id"); + String label = (String) map.get("label"); + int widthMils = (int) map.get("widthMils"); + int heightMils = (int) map.get("heightMils"); + return new MediaSizeExt(id, label, widthMils, heightMils); + } + + public PrintAttributes.MediaSize toMediaSize() { + return new PrintAttributes.MediaSize( + id, "Custom", widthMils, heightMils + ); + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("id", id); + obj.put("label", label); + obj.put("heightMils", heightMils); + obj.put("widthMils", widthMils); + return obj; + } + + @NonNull + public String getId() { + return id; + } + + public void setId(@NonNull String id) { + this.id = id; + } + + @Nullable + public String getLabel() { + return label; + } + + public void setLabel(@Nullable String label) { + this.label = label; + } + + public int getWidthMils() { + return widthMils; + } + + public void setWidthMils(int widthMils) { + this.widthMils = widthMils; + } + + public int getHeightMils() { + return heightMils; + } + + public void setHeightMils(int heightMils) { + this.heightMils = heightMils; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MediaSizeExt that = (MediaSizeExt) o; + + if (widthMils != that.widthMils) return false; + if (heightMils != that.heightMils) return false; + if (!id.equals(that.id)) return false; + return label != null ? label.equals(that.label) : that.label == null; + } + + @Override + public int hashCode() { + int result = id.hashCode(); + result = 31 * result + (label != null ? label.hashCode() : 0); + result = 31 * result + widthMils; + result = 31 * result + heightMils; + return result; + } + + @Override + public String toString() { + return "MediaSizeExt{" + + "id='" + id + '\'' + + ", label='" + label + '\'' + + ", widthMils=" + widthMils + + ", heightMils=" + heightMils + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationAction.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationAction.java new file mode 100644 index 0000000..faf5e59 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationAction.java @@ -0,0 +1,95 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import java.util.HashMap; +import java.util.Map; + +public class NavigationAction { + URLRequest request; + boolean isForMainFrame; + boolean hasGesture; + boolean isRedirect; + + public NavigationAction(URLRequest request, boolean isForMainFrame, boolean hasGesture, boolean isRedirect) { + this.request = request; + this.isForMainFrame = isForMainFrame; + this.hasGesture = hasGesture; + this.isRedirect = isRedirect; + } + + public Map toMap() { + Map navigationActionMap = new HashMap<>(); + navigationActionMap.put("request", request.toMap()); + navigationActionMap.put("isForMainFrame", isForMainFrame); + navigationActionMap.put("hasGesture", hasGesture); + navigationActionMap.put("isRedirect", isRedirect); + navigationActionMap.put("navigationType", null); + navigationActionMap.put("sourceFrame", null); + navigationActionMap.put("targetFrame", null); + navigationActionMap.put("shouldPerformDownload", null); + return navigationActionMap; + } + + public URLRequest getRequest() { + return request; + } + + public void setRequest(URLRequest request) { + this.request = request; + } + + public boolean isForMainFrame() { + return isForMainFrame; + } + + public void setForMainFrame(boolean forMainFrame) { + isForMainFrame = forMainFrame; + } + + public boolean isHasGesture() { + return hasGesture; + } + + public void setHasGesture(boolean hasGesture) { + this.hasGesture = hasGesture; + } + + public boolean isRedirect() { + return isRedirect; + } + + public void setRedirect(boolean redirect) { + isRedirect = redirect; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NavigationAction that = (NavigationAction) o; + + if (isForMainFrame != that.isForMainFrame) return false; + if (hasGesture != that.hasGesture) return false; + if (isRedirect != that.isRedirect) return false; + return request.equals(that.request); + } + + @Override + public int hashCode() { + int result = request.hashCode(); + result = 31 * result + (isForMainFrame ? 1 : 0); + result = 31 * result + (hasGesture ? 1 : 0); + result = 31 * result + (isRedirect ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "NavigationAction{" + + "request=" + request + + ", isForMainFrame=" + isForMainFrame + + ", hasGesture=" + hasGesture + + ", isRedirect=" + isRedirect + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationActionPolicy.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationActionPolicy.java new file mode 100644 index 0000000..c6e0fbb --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationActionPolicy.java @@ -0,0 +1,33 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +public enum NavigationActionPolicy { + CANCEL(0), + ALLOW(1); + + private final int value; + + private NavigationActionPolicy(int value) { + this.value = value; + } + + public boolean equalsValue(int otherValue) { + return value == otherValue; + } + + public static NavigationActionPolicy fromValue(int value) { + for(NavigationActionPolicy type : NavigationActionPolicy.values()) { + if(value == type.value) + return type; + } + throw new IllegalArgumentException("No enum constant: " + value); + } + + public int rawValue() { + return this.value; + } + + @Override + public String toString() { + return String.valueOf(this.value); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PermissionResponse.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PermissionResponse.java new file mode 100644 index 0000000..f3848dd --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PermissionResponse.java @@ -0,0 +1,73 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; +import java.util.Map; + +public class PermissionResponse { + @NonNull + private List resources; + @Nullable + private Integer action; + + public PermissionResponse(@NonNull List resources, @Nullable Integer action) { + this.resources = resources; + this.action = action; + } + + @Nullable + public static PermissionResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + List resources = (List) map.get("resources"); + Integer action = (Integer) map.get("action"); + return new PermissionResponse(resources, action); + } + + @NonNull + public List getResources() { + return resources; + } + + public void setResources(@NonNull List resources) { + this.resources = resources; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PermissionResponse that = (PermissionResponse) o; + + if (!resources.equals(that.resources)) return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = resources.hashCode(); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "PermissionResponse{" + + "resources=" + resources + + ", action=" + action + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PluginScript.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PluginScript.java new file mode 100644 index 0000000..b16f228 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PluginScript.java @@ -0,0 +1,48 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Set; + +public class PluginScript extends UserScript { + private boolean requiredInAllContentWorlds; + + public PluginScript(@Nullable String groupName, @NonNull String source, @NonNull UserScriptInjectionTime injectionTime, @Nullable ContentWorld contentWorld, boolean requiredInAllContentWorlds, @Nullable Set allowedOriginRules) { + super(groupName, source, injectionTime, contentWorld, allowedOriginRules); + this.requiredInAllContentWorlds = requiredInAllContentWorlds; + } + + public boolean isRequiredInAllContentWorlds() { + return requiredInAllContentWorlds; + } + + public void setRequiredInAllContentWorlds(boolean requiredInAllContentWorlds) { + this.requiredInAllContentWorlds = requiredInAllContentWorlds; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + PluginScript that = (PluginScript) o; + + return requiredInAllContentWorlds == that.requiredInAllContentWorlds; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (requiredInAllContentWorlds ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "PluginScript{" + + "requiredInContentWorld=" + requiredInAllContentWorlds + + "} " + super.toString(); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PreferredContentModeOptionType.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PreferredContentModeOptionType.java new file mode 100644 index 0000000..bb5bfa0 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PreferredContentModeOptionType.java @@ -0,0 +1,29 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +public enum PreferredContentModeOptionType { + RECOMMENDED (0), + MOBILE (1), + DESKTOP (2); + + private final int value; + + private PreferredContentModeOptionType(int value) { + this.value = value; + } + + public boolean equalsValue(int otherValue) { + return value == otherValue; + } + + public static PreferredContentModeOptionType fromValue(int value) { + for( PreferredContentModeOptionType type : PreferredContentModeOptionType.values()) { + if(value == type.toValue()) + return type; + } + throw new IllegalArgumentException("No enum constant: " + value); + } + + public int toValue() { + return this.value; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintAttributesExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintAttributesExt.java new file mode 100644 index 0000000..77b6552 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintAttributesExt.java @@ -0,0 +1,56 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.print.PrintAttributes; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.HashMap; +import java.util.Map; + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class PrintAttributesExt { + private int colorMode; + @Nullable + private Integer duplex; + @Nullable + private Integer orientation; + @Nullable + private MediaSizeExt mediaSize; + @Nullable + private ResolutionExt resolution; + @Nullable + private MarginsExt margins; + + @Nullable + public static PrintAttributesExt fromPrintAttributes(@Nullable PrintAttributes attributes) { + if (attributes == null) { + return null; + } + PrintAttributesExt attributesExt = new PrintAttributesExt(); + attributesExt.colorMode = attributes.getColorMode(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + attributesExt.duplex = attributes.getDuplexMode(); + } + PrintAttributes.MediaSize mediaSize = attributes.getMediaSize(); + if (mediaSize != null) { + attributesExt.mediaSize = MediaSizeExt.fromMediaSize(mediaSize); + attributesExt.orientation = mediaSize.isPortrait() ? 0 : 1; + } + attributesExt.resolution = ResolutionExt.fromResolution(attributes.getResolution()); + attributesExt.margins = MarginsExt.fromMargins(attributes.getMinMargins()); + return attributesExt; + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("colorMode", colorMode); + obj.put("duplex", duplex); + obj.put("orientation", orientation); + obj.put("mediaSize", mediaSize != null ? mediaSize.toMap() : null); + obj.put("resolution", resolution != null ? resolution.toMap() : null); + obj.put("margins", margins != null ? margins.toMap() : null); + return obj; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintJobInfoExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintJobInfoExt.java new file mode 100644 index 0000000..e58d851 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintJobInfoExt.java @@ -0,0 +1,56 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.print.PrintJobInfo; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.HashMap; +import java.util.Map; + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class PrintJobInfoExt { + private int state; + private int copies; + @Nullable + private Integer numberOfPages; + private long creationTime; + @NonNull + private String label; + @Nullable + private String printerId; + @Nullable + private PrintAttributesExt attributes; + + @Nullable + public static PrintJobInfoExt fromPrintJobInfo(@Nullable PrintJobInfo info) { + if (info == null) { + return null; + } + PrintJobInfoExt printJobInfoExt = new PrintJobInfoExt(); + printJobInfoExt.state = info.getState(); + printJobInfoExt.copies = info.getCopies(); + printJobInfoExt.numberOfPages = info.getPages() != null ? info.getPages().length : null; + printJobInfoExt.creationTime = info.getCreationTime(); + printJobInfoExt.label = info.getLabel(); + printJobInfoExt.printerId = info.getPrinterId() != null ? info.getPrinterId().getLocalId() : null; + printJobInfoExt.attributes = PrintAttributesExt.fromPrintAttributes(info.getAttributes()); + return printJobInfoExt; + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("state", state); + obj.put("copies", copies); + obj.put("numberOfPages", numberOfPages); + obj.put("creationTime", creationTime); + obj.put("label", label); + Map printer = new HashMap<>(); + printer.put("id", printerId); + obj.put("printer", printer); + obj.put("attributes", attributes != null ? attributes.toMap() : null); + return obj; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ProxyRuleExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ProxyRuleExt.java new file mode 100644 index 0000000..52081a3 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ProxyRuleExt.java @@ -0,0 +1,83 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.ProxyConfig; + +import java.util.HashMap; +import java.util.Map; + +public class ProxyRuleExt { + @Nullable + @ProxyConfig.ProxyScheme + private String schemeFilter; + @NonNull + private String url; + + public ProxyRuleExt(@Nullable @ProxyConfig.ProxyScheme String schemeFilter, @NonNull String url) { + this.schemeFilter = schemeFilter; + this.url = url; + } + + @Nullable + public static ProxyRuleExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String url = (String) map.get("url"); + String schemeFilter = (String) map.get("schemeFilter"); + return new ProxyRuleExt(schemeFilter, url); + } + + public Map toMap() { + Map proxyRuleMap = new HashMap<>(); + proxyRuleMap.put("url", url); + proxyRuleMap.put("schemeFilter", schemeFilter); + return proxyRuleMap; + } + + @Nullable + public String getSchemeFilter() { + return schemeFilter; + } + + public void setSchemeFilter(@Nullable String schemeFilter) { + this.schemeFilter = schemeFilter; + } + + @NonNull + public String getUrl() { + return url; + } + + public void setUrl(@NonNull String url) { + this.url = url; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ProxyRuleExt that = (ProxyRuleExt) o; + + if (schemeFilter != null ? !schemeFilter.equals(that.schemeFilter) : that.schemeFilter != null) + return false; + return url.equals(that.url); + } + + @Override + public int hashCode() { + int result = schemeFilter != null ? schemeFilter.hashCode() : 0; + result = 31 * result + url.hashCode(); + return result; + } + + @Override + public String toString() { + return "ProxyRuleExt{" + + "schemeFilter='" + schemeFilter + '\'' + + ", url='" + url + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ResolutionExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ResolutionExt.java new file mode 100644 index 0000000..daca111 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ResolutionExt.java @@ -0,0 +1,134 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.print.PrintAttributes; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.HashMap; +import java.util.Map; + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class ResolutionExt { + @NonNull + private String id; + @NonNull + private String label; + private int verticalDpi; + private int horizontalDpi; + + public ResolutionExt(@NonNull String id, @NonNull String label, int verticalDpi, int horizontalDpi) { + this.id = id; + this.label = label; + this.verticalDpi = verticalDpi; + this.horizontalDpi = horizontalDpi; + } + + @Nullable + public static ResolutionExt fromResolution(@Nullable PrintAttributes.Resolution resolution) { + if (resolution == null) { + return null; + } + return new ResolutionExt( + resolution.getId(), + resolution.getLabel(), + resolution.getVerticalDpi(), + resolution.getHorizontalDpi() + ); + } + + @Nullable + public static ResolutionExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String id = (String) map.get("id"); + String label = (String) map.get("label"); + int verticalDpi = (int) map.get("verticalDpi"); + int horizontalDpi = (int) map.get("horizontalDpi"); + return new ResolutionExt(id, label, verticalDpi, horizontalDpi); + } + + public PrintAttributes.Resolution toResolution() { + return new PrintAttributes.Resolution( + id, label, horizontalDpi, verticalDpi + ); + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("id", id); + obj.put("label", label); + obj.put("verticalDpi", verticalDpi); + obj.put("horizontalDpi", horizontalDpi); + return obj; + } + + @NonNull + public String getId() { + return id; + } + + public void setId(@NonNull String id) { + this.id = id; + } + + @NonNull + public String getLabel() { + return label; + } + + public void setLabel(@NonNull String label) { + this.label = label; + } + + public int getVerticalDpi() { + return verticalDpi; + } + + public void setVerticalDpi(int verticalDpi) { + this.verticalDpi = verticalDpi; + } + + public int getHorizontalDpi() { + return horizontalDpi; + } + + public void setHorizontalDpi(int horizontalDpi) { + this.horizontalDpi = horizontalDpi; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ResolutionExt that = (ResolutionExt) o; + + if (verticalDpi != that.verticalDpi) return false; + if (horizontalDpi != that.horizontalDpi) return false; + if (!id.equals(that.id)) return false; + return label.equals(that.label); + } + + @Override + public int hashCode() { + int result = id.hashCode(); + result = 31 * result + label.hashCode(); + result = 31 * result + verticalDpi; + result = 31 * result + horizontalDpi; + return result; + } + + @Override + public String toString() { + return "ResolutionExt{" + + "id='" + id + '\'' + + ", label='" + label + '\'' + + ", verticalDpi=" + verticalDpi + + ", horizontalDpi=" + horizontalDpi + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SafeBrowsingResponse.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SafeBrowsingResponse.java new file mode 100644 index 0000000..765fe25 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SafeBrowsingResponse.java @@ -0,0 +1,69 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class SafeBrowsingResponse { + private boolean report; + @Nullable + private Integer action; + + public SafeBrowsingResponse(boolean report, @Nullable Integer action) { + this.report = report; + this.action = action; + } + + @Nullable + public static SafeBrowsingResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + boolean report = (boolean) map.get("report"); + Integer action = (Integer) map.get("action"); + return new SafeBrowsingResponse(report, action); + } + + public boolean isReport() { + return report; + } + + public void setReport(boolean report) { + this.report = report; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SafeBrowsingResponse that = (SafeBrowsingResponse) o; + + if (report != that.report) return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = (report ? 1 : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "SafeBrowsingResponse{" + + "report=" + report + + ", action=" + action + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustAuthResponse.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustAuthResponse.java new file mode 100644 index 0000000..eebf7bd --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustAuthResponse.java @@ -0,0 +1,54 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class ServerTrustAuthResponse { + @Nullable + private Integer action; + + public ServerTrustAuthResponse(@Nullable Integer action) { + this.action = action; + } + + @Nullable + public static ServerTrustAuthResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + Integer action = (Integer) map.get("action"); + return new ServerTrustAuthResponse(action); + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ServerTrustAuthResponse that = (ServerTrustAuthResponse) o; + + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + return action != null ? action.hashCode() : 0; + } + + @Override + public String toString() { + return "ServerTrustAuthResponse{" + + "action=" + action + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustChallenge.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustChallenge.java new file mode 100644 index 0000000..1761645 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustChallenge.java @@ -0,0 +1,12 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +public class ServerTrustChallenge extends URLAuthenticationChallenge { + public ServerTrustChallenge(URLProtectionSpace protectionSpace) { + super(protectionSpace); + } + + @Override + public String toString() { + return "ServerTrustChallenge{} " + super.toString(); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Size2D.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Size2D.java new file mode 100644 index 0000000..da51269 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Size2D.java @@ -0,0 +1,81 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class Size2D { + private double width; + private double height; + + public Size2D(double width, double height) { + this.width = width; + this.height = height; + } + + @Nullable + public static Size2D fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + Double width = (Double) map.get("width"); + Double height = (Double) map.get("height"); + assert width != null; + assert height != null; + return new Size2D(width, height); + } + + public Map toMap() { + Map sizeMap = new HashMap<>(); + sizeMap.put("width", width); + sizeMap.put("height", height); + return sizeMap; + } + + public double getWidth() { + return width; + } + + public void setWidth(double width) { + this.width = width; + } + + public double getHeight() { + return height; + } + + public void setHeight(double height) { + this.height = height; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Size2D size = (Size2D) o; + + if (Double.compare(size.width, width) != 0) return false; + return Double.compare(size.height, height) == 0; + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(width); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(height); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "Size{" + + "width=" + width + + ", height=" + height + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslCertificateExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslCertificateExt.java new file mode 100644 index 0000000..3208745 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslCertificateExt.java @@ -0,0 +1,77 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.net.http.SslCertificate; +import android.os.Build; + +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + +public class SslCertificateExt extends SslCertificate { + + private SslCertificateExt(X509Certificate certificate) { + super(certificate); + } + + @Nullable + static public Map toMap(@Nullable SslCertificate sslCertificate) { + if (sslCertificate == null) { + return null; + } + + DName issuedByName = sslCertificate.getIssuedBy(); + Map issuedBy = null; + if (issuedByName != null) { + issuedBy = new HashMap<>(); + issuedBy.put("CName", issuedByName.getCName()); + issuedBy.put("DName", issuedByName.getDName()); + issuedBy.put("OName", issuedByName.getOName()); + issuedBy.put("UName", issuedByName.getUName()); + } + + DName issuedToName = sslCertificate.getIssuedTo(); + Map issuedTo = null; + if (issuedToName != null) { + issuedTo = new HashMap<>(); + issuedTo.put("CName", issuedToName.getCName()); + issuedTo.put("DName", issuedToName.getDName()); + issuedTo.put("OName", issuedToName.getOName()); + issuedTo.put("UName", issuedToName.getUName()); + } + + byte[] x509CertificateData = null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + X509Certificate certificate = sslCertificate.getX509Certificate(); + if (certificate != null) { + x509CertificateData = certificate.getEncoded(); + } + } catch (CertificateEncodingException e) { + e.printStackTrace(); + } + } else { + try { + x509CertificateData = Util.getX509CertFromSslCertHack(sslCertificate).getEncoded(); + } catch (CertificateEncodingException e) { + e.printStackTrace(); + } + } + + long validNotAfterDate = sslCertificate.getValidNotAfterDate().getTime(); + long validNotBeforeDate = sslCertificate.getValidNotBeforeDate().getTime(); + + Map sslCertificateMap = new HashMap<>(); + sslCertificateMap.put("issuedBy", issuedBy); + sslCertificateMap.put("issuedTo", issuedTo); + sslCertificateMap.put("validNotAfterDate", validNotAfterDate); + sslCertificateMap.put("validNotBeforeDate", validNotBeforeDate); + sslCertificateMap.put("x509Certificate", x509CertificateData); + return sslCertificateMap; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslErrorExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslErrorExt.java new file mode 100644 index 0000000..f883132 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslErrorExt.java @@ -0,0 +1,56 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.net.http.SslCertificate; +import android.net.http.SslError; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class SslErrorExt extends SslError { + + private SslErrorExt(int error, SslCertificate certificate, String url) { + super(error, certificate, url); + } + + @Nullable + static public Map toMap(SslError sslError) { + if (sslError == null) { + return null; + } + + int primaryError = sslError.getPrimaryError(); + + String message; + switch (primaryError) { + case SslError.SSL_DATE_INVALID: + message = "The date of the certificate is invalid"; + break; + case SslError.SSL_EXPIRED: + message = "The certificate has expired"; + break; + case SslError.SSL_IDMISMATCH: + message = "Hostname mismatch"; + break; + case SslError.SSL_INVALID: + message = "A generic error occurred"; + break; + case SslError.SSL_NOTYETVALID: + message = "The certificate is not yet valid"; + break; + case SslError.SSL_UNTRUSTED: + message = "The certificate authority is not trusted"; + break; + default: + message = null; + break; + } + + Map urlProtectionSpaceMap = new HashMap<>(); + urlProtectionSpaceMap.put("code", primaryError >= 0 ? primaryError : null); + urlProtectionSpaceMap.put("message", message); + return urlProtectionSpaceMap; + } + +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SyncBaseCallbackResultImpl.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SyncBaseCallbackResultImpl.java new file mode 100644 index 0000000..1fd2ee7 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SyncBaseCallbackResultImpl.java @@ -0,0 +1,47 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; + +import java.util.concurrent.CountDownLatch; + +public class SyncBaseCallbackResultImpl extends BaseCallbackResultImpl { + final public CountDownLatch latch = new CountDownLatch(1); + @Nullable + public T result = null; + + @CallSuper + @Override + public void defaultBehaviour(@Nullable T result) { + latch.countDown(); + } + + @Override + public void success(@Nullable Object obj) { + T result = decodeResult(obj); + this.result = result; + boolean shouldRunDefaultBehaviour; + if (result == null) { + shouldRunDefaultBehaviour = nullSuccess(); + } else { + shouldRunDefaultBehaviour = nonNullSuccess(result); + } + if (shouldRunDefaultBehaviour) { + defaultBehaviour(result); + } else { + latch.countDown(); + } + } + + @CallSuper + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + latch.countDown(); + } + + @CallSuper + @Override + public void notImplemented() { + defaultBehaviour(null); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLAuthenticationChallenge.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLAuthenticationChallenge.java new file mode 100644 index 0000000..7d01507 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLAuthenticationChallenge.java @@ -0,0 +1,48 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import java.util.HashMap; +import java.util.Map; + +public class URLAuthenticationChallenge { + private URLProtectionSpace protectionSpace; + + public URLAuthenticationChallenge(URLProtectionSpace protectionSpace) { + this.protectionSpace = protectionSpace; + } + + public Map toMap() { + Map challengeMap = new HashMap<>(); + challengeMap.put("protectionSpace", protectionSpace.toMap()); + return challengeMap; + } + + public URLProtectionSpace getProtectionSpace() { + return protectionSpace; + } + + public void setProtectionSpace(URLProtectionSpace protectionSpace) { + this.protectionSpace = protectionSpace; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLAuthenticationChallenge challenge = (URLAuthenticationChallenge) o; + + return protectionSpace.equals(challenge.protectionSpace); + } + + @Override + public int hashCode() { + return protectionSpace.hashCode(); + } + + @Override + public String toString() { + return "URLAuthenticationChallenge{" + + "protectionSpace=" + protectionSpace + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLCredential.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLCredential.java new file mode 100644 index 0000000..0bf7251 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLCredential.java @@ -0,0 +1,101 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class URLCredential { + @Nullable + private Long id; + @Nullable + private String username; + @Nullable + private String password; + @Nullable + private Long protectionSpaceId; + + public URLCredential(@Nullable String username, @Nullable String password) { + this.username = username; + this.password = password; + } + + public URLCredential (@Nullable Long id, @NonNull String username, @NonNull String password, @Nullable Long protectionSpaceId) { + this.id = id; + this.username = username; + this.password = password; + this.protectionSpaceId = protectionSpaceId; + } + + public Map toMap() { + Map urlCredentialMap = new HashMap<>(); + urlCredentialMap.put("username", username); + urlCredentialMap.put("password", password); + urlCredentialMap.put("certificates", null); + urlCredentialMap.put("persistence", null); + return urlCredentialMap; + } + + @Nullable + public Long getId() { + return id; + } + + public void setId(@Nullable Long id) { + this.id = id; + } + + @Nullable + public String getUsername() { + return username; + } + + public void setUsername(@Nullable String username) { + this.username = username; + } + + @Nullable + public String getPassword() { + return password; + } + + public void setPassword(@Nullable String password) { + this.password = password; + } + + @Nullable + public Long getProtectionSpaceId() { + return protectionSpaceId; + } + + public void setProtectionSpaceId(@Nullable Long protectionSpaceId) { + this.protectionSpaceId = protectionSpaceId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLCredential that = (URLCredential) o; + + if (username != null ? !username.equals(that.username) : that.username != null) return false; + return password != null ? password.equals(that.password) : that.password == null; + } + + @Override + public int hashCode() { + int result = username != null ? username.hashCode() : 0; + result = 31 * result + (password != null ? password.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "URLCredential{" + + "username='" + username + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLProtectionSpace.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLProtectionSpace.java new file mode 100644 index 0000000..a27e97f --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLProtectionSpace.java @@ -0,0 +1,155 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.net.http.SslCertificate; +import android.net.http.SslError; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class URLProtectionSpace { + @Nullable + private Long id; + private String host; + private String protocol; + @Nullable + private String realm; + private int port; + @Nullable + private SslCertificate sslCertificate; + @Nullable + private SslError sslError; + + public URLProtectionSpace(String host, String protocol, @Nullable String realm, int port, @Nullable SslCertificate sslCertificate, @Nullable SslError sslError) { + this.host = host; + this.protocol = protocol; + this.realm = realm; + this.port = port; + this.sslCertificate = sslCertificate; + this.sslError = sslError; + } + + public URLProtectionSpace(@Nullable Long id, String host, String protocol, @Nullable String realm, int port) { + this.id = id; + this.host = host; + this.protocol = protocol; + this.realm = realm; + this.port = port; + } + + public Map toMap() { + Map urlProtectionSpaceMap = new HashMap<>(); + urlProtectionSpaceMap.put("host", host); + urlProtectionSpaceMap.put("protocol", protocol); + urlProtectionSpaceMap.put("realm", realm); + urlProtectionSpaceMap.put("port", port); + urlProtectionSpaceMap.put("sslCertificate", SslCertificateExt.toMap(sslCertificate)); + urlProtectionSpaceMap.put("sslError", SslErrorExt.toMap(sslError)); + urlProtectionSpaceMap.put("authenticationMethod", null); + urlProtectionSpaceMap.put("distinguishedNames", null); + urlProtectionSpaceMap.put("receivesCredentialSecurely", null); + urlProtectionSpaceMap.put("isProxy", null); + urlProtectionSpaceMap.put("proxyType", null); + return urlProtectionSpaceMap; + } + + @Nullable + public Long getId() { + return id; + } + + public void setId(@Nullable Long id) { + this.id = id; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + @Nullable + public String getRealm() { + return realm; + } + + public void setRealm(@Nullable String realm) { + this.realm = realm; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + @Nullable + public SslCertificate getSslCertificate() { + return sslCertificate; + } + + public void setSslCertificate(@Nullable SslCertificate sslCertificateExt) { + this.sslCertificate = sslCertificateExt; + } + + @Nullable + public SslError getSslError() { + return sslError; + } + + public void setSslError(@Nullable SslError sslError) { + this.sslError = sslError; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLProtectionSpace that = (URLProtectionSpace) o; + + if (port != that.port) return false; + if (!host.equals(that.host)) return false; + if (!protocol.equals(that.protocol)) return false; + if (realm != null ? !realm.equals(that.realm) : that.realm != null) return false; + if (sslCertificate != null ? !sslCertificate.equals(that.sslCertificate) : that.sslCertificate != null) + return false; + return sslError != null ? sslError.equals(that.sslError) : that.sslError == null; + } + + @Override + public int hashCode() { + int result = host.hashCode(); + result = 31 * result + protocol.hashCode(); + result = 31 * result + (realm != null ? realm.hashCode() : 0); + result = 31 * result + port; + result = 31 * result + (sslCertificate != null ? sslCertificate.hashCode() : 0); + result = 31 * result + (sslError != null ? sslError.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "URLProtectionSpace{" + + "host='" + host + '\'' + + ", protocol='" + protocol + '\'' + + ", realm='" + realm + '\'' + + ", port=" + port + + ", sslCertificate=" + sslCertificate + + ", sslError=" + sslError + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLRequest.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLRequest.java new file mode 100644 index 0000000..d9d598d --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLRequest.java @@ -0,0 +1,128 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class URLRequest { + @Nullable + private String url; + @Nullable + private String method; + @Nullable + private byte[] body; + @Nullable + private Map headers; + + public URLRequest(@Nullable String url, @Nullable String method, @Nullable byte[] body, @Nullable Map headers) { + this.url = url; + this.method = method; + this.body = body; + this.headers = headers; + } + + @Nullable + public static URLRequest fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String url = (String) map.get("url"); + if (url == null) { + url = "about:blank"; + } + String method = (String) map.get("method"); + byte[] body = (byte[]) map.get("body"); + Map headers = (Map) map.get("headers"); + return new URLRequest(url, method, body, headers); + } + + public Map toMap() { + Map urlRequestMap = new HashMap<>(); + urlRequestMap.put("url", url); + urlRequestMap.put("method", method); + urlRequestMap.put("headers", headers); + urlRequestMap.put("body", body); + urlRequestMap.put("allowsCellularAccess", null); + urlRequestMap.put("allowsConstrainedNetworkAccess", null); + urlRequestMap.put("allowsExpensiveNetworkAccess", null); + urlRequestMap.put("cachePolicy", null); + urlRequestMap.put("httpShouldHandleCookies", null); + urlRequestMap.put("httpShouldUsePipelining", null); + urlRequestMap.put("networkServiceType", null); + urlRequestMap.put("timeoutInterval", null); + urlRequestMap.put("mainDocumentURL", null); + urlRequestMap.put("assumesHTTP3Capable", null); + urlRequestMap.put("attribution", null); + return urlRequestMap; + } + + @Nullable + public String getUrl() { + return url; + } + + public void setUrl(@Nullable String url) { + this.url = url; + } + + @Nullable + public String getMethod() { + return method; + } + + public void setMethod(@Nullable String method) { + this.method = method; + } + + @Nullable + public byte[] getBody() { + return body; + } + + public void setBody(@Nullable byte[] body) { + this.body = body; + } + + @Nullable + public Map getHeaders() { + return headers; + } + + public void setHeaders(@Nullable Map headers) { + this.headers = headers; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLRequest that = (URLRequest) o; + + if (url != null ? !url.equals(that.url) : that.url != null) return false; + if (method != null ? !method.equals(that.method) : that.method != null) return false; + if (!Arrays.equals(body, that.body)) return false; + return headers != null ? headers.equals(that.headers) : that.headers == null; + } + + @Override + public int hashCode() { + int result = url != null ? url.hashCode() : 0; + result = 31 * result + (method != null ? method.hashCode() : 0); + result = 31 * result + Arrays.hashCode(body); + result = 31 * result + (headers != null ? headers.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "URLRequest{" + + "url='" + url + '\'' + + ", method='" + method + '\'' + + ", body=" + Arrays.toString(body) + + ", headers=" + headers + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserContentController.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserContentController.java new file mode 100644 index 0000000..f51dd1b --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserContentController.java @@ -0,0 +1,473 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.annotation.SuppressLint; +import android.text.TextUtils; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.ScriptHandler; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.PluginScriptsUtil; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@SuppressLint("RestrictedApi") +public class UserContentController implements Disposable { + protected static final String LOG_TAG = "UserContentController"; + + @NonNull + private final Set contentWorlds = new HashSet() {{ + add(ContentWorld.PAGE); + }}; + + private final Map scriptHandlerMap = new HashMap<>(); + + @Nullable + private ScriptHandler contentWorldsCreatorScript; + + @NonNull + private final Map> userOnlyScripts = new HashMap>() {{ + put(UserScriptInjectionTime.AT_DOCUMENT_START, new LinkedHashSet()); + put(UserScriptInjectionTime.AT_DOCUMENT_END, new LinkedHashSet()); + }}; + @NonNull + private final Map> pluginScripts = new HashMap>() {{ + put(UserScriptInjectionTime.AT_DOCUMENT_START, new LinkedHashSet()); + put(UserScriptInjectionTime.AT_DOCUMENT_END, new LinkedHashSet()); + }}; + + @Nullable + public WebView webView; + + public UserContentController(WebView webView) { + this.webView = webView; + } + + public String generateWrappedCodeForDocumentStart() { + return Util.replaceAll( + DOCUMENT_READY_WRAPPER_JS_SOURCE, + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, + generateCodeForDocumentStart()); + } + + public String generateWrappedCodeForDocumentEnd() { + UserScriptInjectionTime injectionTime = UserScriptInjectionTime.AT_DOCUMENT_END; + String js = ""; + if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + // try to reload scripts if they were not loaded during the AT_DOCUMENT_START event + js += generateCodeForDocumentStart(); + } + js += generatePluginScriptsCodeAt(injectionTime); + js += generateUserOnlyScriptsCodeAt(injectionTime); + js = USER_SCRIPTS_AT_DOCUMENT_END_WRAPPER_JS_SOURCE.replace(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, js); + return js; + } + + public String generateCodeForDocumentStart() { + UserScriptInjectionTime injectionTime = UserScriptInjectionTime.AT_DOCUMENT_START; + String js = ""; + js += generatePluginScriptsCodeAt(injectionTime); + js += generateContentWorldsCreatorCode(); + js += generateUserOnlyScriptsCodeAt(injectionTime); + js = USER_SCRIPTS_AT_DOCUMENT_START_WRAPPER_JS_SOURCE.replace(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, js); + return js; + } + + public String generateContentWorldsCreatorCode() { + if (this.contentWorlds.size() == 1) { + return ""; + } + + StringBuilder source = new StringBuilder(); + LinkedHashSet pluginScriptsRequired = this.getPluginScriptsRequiredInAllContentWorlds(); + for (PluginScript script : pluginScriptsRequired) { + source.append(script.getSource()); + } + List contentWorldsNames = new ArrayList<>(); + for (ContentWorld contentWorld : this.contentWorlds) { + if (contentWorld.equals(ContentWorld.PAGE)) { + continue; + } + contentWorldsNames.add("'" + escapeContentWorldName(contentWorld.getName()) + "'"); + } + + return CONTENT_WORLDS_GENERATOR_JS_SOURCE + .replace(PluginScriptsUtil.VAR_CONTENT_WORLD_NAME_ARRAY, TextUtils.join(", ", contentWorldsNames)) + .replace(PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED, escapeCode(source.toString())); + + } + + public String generatePluginScriptsCodeAt(UserScriptInjectionTime injectionTime) { + StringBuilder js = new StringBuilder(); + LinkedHashSet scripts = this.getPluginScriptsAt(injectionTime); + for (PluginScript script : scripts) { + String source = ";" + script.getSource(); + source = wrapSourceCodeInContentWorld(script.getContentWorld(), source); + js.append(source); + } + return js.toString(); + } + + public String generateUserOnlyScriptsCodeAt(UserScriptInjectionTime injectionTime) { + StringBuilder js = new StringBuilder(); + LinkedHashSet scripts = this.getUserOnlyScriptsAt(injectionTime); + for (UserScript script : scripts) { + String source = ";" + script.getSource(); + source = wrapSourceCodeInContentWorld(script.getContentWorld(), source); + js.append(source); + } + return js.toString(); + } + + public String generateCodeForScriptEvaluation(String source, @Nullable ContentWorld contentWorld) { + if (contentWorld != null && !contentWorld.equals(ContentWorld.PAGE)) { + StringBuilder sourceWrapped = new StringBuilder(); + if (!contentWorlds.contains(contentWorld)) { + contentWorlds.add(contentWorld); + + StringBuilder pluginScriptsSource = new StringBuilder(); + LinkedHashSet pluginScriptsRequired = this.getPluginScriptsRequiredInAllContentWorlds(); + for (PluginScript script : pluginScriptsRequired) { + pluginScriptsSource.append(script.getSource()); + } + String contentWorldCreatorCode = CONTENT_WORLDS_GENERATOR_JS_SOURCE + .replace(PluginScriptsUtil.VAR_CONTENT_WORLD_NAME_ARRAY, "'" + escapeContentWorldName(contentWorld.getName()) + "'") + .replace(PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED, escapeCode(pluginScriptsSource.toString())); + sourceWrapped.append(contentWorldCreatorCode).append(";"); + } + return sourceWrapped.append(wrapSourceCodeInContentWorld(contentWorld, source)).toString(); + } + return source; + } + + public String wrapSourceCodeInContentWorld(@Nullable ContentWorld contentWorld, String source) { + String sourceWrapped = contentWorld == null || contentWorld.equals(ContentWorld.PAGE) ? source : + CONTENT_WORLD_WRAPPER_JS_SOURCE + .replace(PluginScriptsUtil.VAR_CONTENT_WORLD_NAME, escapeContentWorldName(contentWorld.getName())) + .replace(PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED, escapeCode(source)); + + return sourceWrapped; + } + + public static String escapeCode(String code) { + String escapedCode = JSONObject.quote(code); + // escapedCode = escapedCode.substring(1, escapedCode.length() - 1); + return escapedCode; + } + + public static String escapeContentWorldName(String name) { + return name.replaceAll("'", "\\\\'"); + } + + public LinkedHashSet getUserOnlyScriptsAt(UserScriptInjectionTime injectionTime) { + return new LinkedHashSet<>(this.userOnlyScripts.get(injectionTime)); + } + + private void updateContentWorldsCreatorScript() { + String source = generateContentWorldsCreatorCode(); + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + if (contentWorldsCreatorScript != null) { + contentWorldsCreatorScript.remove(); + } + if (!source.isEmpty() && webView != null) { + contentWorldsCreatorScript = WebViewCompat.addDocumentStartJavaScript( + webView, + source, + new HashSet() {{ + add("*"); + }} + ); + } + } + } + + public boolean addUserOnlyScript(UserScript userOnlyScript) { + ContentWorld contentWorld = userOnlyScript.getContentWorld(); + if (contentWorld != null) { + contentWorlds.add(contentWorld); + } + this.updateContentWorldsCreatorScript(); + if (webView != null && userOnlyScript.getInjectionTime() == UserScriptInjectionTime.AT_DOCUMENT_START + && WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + ScriptHandler scriptHandler = WebViewCompat.addDocumentStartJavaScript( + webView, + wrapSourceCodeInContentWorld(userOnlyScript.getContentWorld(), userOnlyScript.getSource()), + userOnlyScript.getAllowedOriginRules() + ); + this.scriptHandlerMap.put(userOnlyScript, scriptHandler); + } + return this.userOnlyScripts.get(userOnlyScript.getInjectionTime()).add(userOnlyScript); + } + + public void addUserOnlyScripts(List userOnlyScripts) { + for (UserScript userOnlyScript : userOnlyScripts) { + this.addUserOnlyScript(userOnlyScript); + } + } + + public boolean removeUserOnlyScript(UserScript userOnlyScript) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + ScriptHandler scriptHandler = this.scriptHandlerMap.get(userOnlyScript); + if (scriptHandler != null) { + scriptHandler.remove(); + this.scriptHandlerMap.remove(userOnlyScript); + } + this.updateContentWorldsCreatorScript(); + } + return this.userOnlyScripts.get(userOnlyScript.getInjectionTime()).remove(userOnlyScript); + } + + public boolean removeUserOnlyScriptAt(int index, UserScriptInjectionTime injectionTime) { + UserScript userOnlyScript = new ArrayList<>(this.userOnlyScripts.get(injectionTime)).get(index); + return this.removeUserOnlyScript(userOnlyScript); + } + + public void removeAllUserOnlyScripts() { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + for (UserScript userOnlyScript : this.userOnlyScripts.get(UserScriptInjectionTime.AT_DOCUMENT_START)) { + ScriptHandler scriptHandler = this.scriptHandlerMap.get(userOnlyScript); + if (scriptHandler != null) { + scriptHandler.remove(); + this.scriptHandlerMap.remove(userOnlyScript); + } + } + } + this.userOnlyScripts.get(UserScriptInjectionTime.AT_DOCUMENT_START).clear(); + this.userOnlyScripts.get(UserScriptInjectionTime.AT_DOCUMENT_END).clear(); + } + + public LinkedHashSet getPluginScriptsAt(UserScriptInjectionTime injectionTime) { + return new LinkedHashSet<>(this.pluginScripts.get(injectionTime)); + } + + public LinkedHashSet getPluginScriptsRequiredInAllContentWorlds() { + LinkedHashSet pluginScriptsRequired = new LinkedHashSet<>(); + LinkedHashSet scripts = this.getPluginScriptsAt(UserScriptInjectionTime.AT_DOCUMENT_START); + for (PluginScript script : scripts) { + if (script.isRequiredInAllContentWorlds()) { + pluginScriptsRequired.add(script); + } + } + return pluginScriptsRequired; + } + + public boolean addPluginScript(PluginScript pluginScript) { + ContentWorld contentWorld = pluginScript.getContentWorld(); + if (contentWorld != null) { + contentWorlds.add(contentWorld); + } + this.updateContentWorldsCreatorScript(); + if (webView != null && pluginScript.getInjectionTime() == UserScriptInjectionTime.AT_DOCUMENT_START + && WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + ScriptHandler scriptHandler = WebViewCompat.addDocumentStartJavaScript( + webView, + wrapSourceCodeInContentWorld(pluginScript.getContentWorld(), pluginScript.getSource()), + pluginScript.getAllowedOriginRules() + ); + this.scriptHandlerMap.put(pluginScript, scriptHandler); + } + return this.pluginScripts.get(pluginScript.getInjectionTime()).add(pluginScript); + } + + public void addPluginScripts(List pluginScripts) { + for (PluginScript pluginScript : pluginScripts) { + this.addPluginScript(pluginScript); + } + } + + public boolean removePluginScript(PluginScript pluginScript) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + ScriptHandler scriptHandler = this.scriptHandlerMap.get(pluginScript); + if (scriptHandler != null) { + scriptHandler.remove(); + this.scriptHandlerMap.remove(pluginScript); + } + this.updateContentWorldsCreatorScript(); + } + return this.pluginScripts.get(pluginScript.getInjectionTime()).remove(pluginScript); + } + + public void removeAllPluginScripts() { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + for (PluginScript pluginScript : this.pluginScripts.get(UserScriptInjectionTime.AT_DOCUMENT_START)) { + ScriptHandler scriptHandler = this.scriptHandlerMap.get(pluginScript); + if (scriptHandler != null) { + scriptHandler.remove(); + this.scriptHandlerMap.remove(pluginScript); + } + } + } + this.pluginScripts.get(UserScriptInjectionTime.AT_DOCUMENT_START).clear(); + this.pluginScripts.get(UserScriptInjectionTime.AT_DOCUMENT_END).clear(); + } + + public LinkedHashSet getUserOnlyScriptAsList() { + LinkedHashSet userOnlyScripts = new LinkedHashSet<>(); + Collection> collection = this.userOnlyScripts.values(); + for (LinkedHashSet list : collection) { + userOnlyScripts.addAll(list); + } + return userOnlyScripts; + } + + public LinkedHashSet getPluginScriptAsList() { + LinkedHashSet pluginScripts = new LinkedHashSet<>(); + Collection> collection = this.pluginScripts.values(); + for (LinkedHashSet list : collection) { + pluginScripts.addAll(list); + } + return pluginScripts; + } + + public void resetContentWorlds() { + this.contentWorlds.clear(); + this.contentWorlds.add(ContentWorld.PAGE); + + LinkedHashSet pluginScripts = this.getPluginScriptAsList(); + for (PluginScript pluginScript : pluginScripts) { + ContentWorld contentWorld = pluginScript.getContentWorld(); + this.contentWorlds.add(contentWorld); + } + + LinkedHashSet userOnlyScripts = this.getUserOnlyScriptAsList(); + for (UserScript userOnlyScript : userOnlyScripts) { + ContentWorld contentWorld = userOnlyScript.getContentWorld(); + this.contentWorlds.add(contentWorld); + } + } + + public boolean containsPluginScript(PluginScript pluginScript) { + return this.getPluginScriptAsList().contains(pluginScript); + } + + public boolean containsPluginScriptByGroupName(String groupName) { + LinkedHashSet pluginScripts = this.getPluginScriptAsList(); + for (PluginScript pluginScript : pluginScripts) { + if (Util.objEquals(groupName, pluginScript.getGroupName())) { + return true; + } + } + return false; + } + + public boolean containsUserOnlyScript(UserScript userOnlyScript) { + return this.getUserOnlyScriptAsList().contains(userOnlyScript); + } + + public boolean containsUserOnlyScriptByGroupName(String groupName) { + LinkedHashSet userOnlyScripts = this.getUserOnlyScriptAsList(); + for (UserScript userOnlyScript : userOnlyScripts) { + if (Util.objEquals(groupName, userOnlyScript.getGroupName())) { + return true; + } + } + + return false; + } + + public void removePluginScriptsByGroupName(String groupName) { + LinkedHashSet pluginScripts = this.getPluginScriptAsList(); + for (PluginScript pluginScript : pluginScripts) { + if (Util.objEquals(groupName, pluginScript.getGroupName())) { + this.removePluginScript(pluginScript); + } + } + } + + public void removeUserOnlyScriptsByGroupName(String groupName) { + LinkedHashSet userOnlyScripts = this.getUserOnlyScriptAsList(); + for (UserScript userOnlyScript : userOnlyScripts) { + if (Util.objEquals(groupName, userOnlyScript.getGroupName())) { + this.removeUserOnlyScript(userOnlyScript); + } + } + } + + @NonNull + public LinkedHashSet getContentWorlds() { + return new LinkedHashSet<>(this.contentWorlds); + } + + private static final String USER_SCRIPTS_AT_DOCUMENT_START_WRAPPER_JS_SOURCE = "if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + " != null && (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentStartLoaded == null || !window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentStartLoaded)) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentStartLoaded = true;" + + " " + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE + + "}"; + + private static final String USER_SCRIPTS_AT_DOCUMENT_END_WRAPPER_JS_SOURCE = "if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + " != null && (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentEndLoaded == null || !window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentEndLoaded)) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentEndLoaded = true;" + + " " + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE + + "}"; + + private static final String CONTENT_WORLDS_GENERATOR_JS_SOURCE = "(function() {" + + " var interval = setInterval(function() {" + + " if (document.body == null) {return;}" + + " var contentWorldNames = [" + PluginScriptsUtil.VAR_CONTENT_WORLD_NAME_ARRAY + "];" + + " for (var contentWorldName of contentWorldNames) {" + + " var iframeId = '" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_' + contentWorldName;" + + " var iframe = document.getElementById(iframeId);" + + " if (iframe == null) {" + + " iframe = document.createElement('iframe');" + + " iframe.id = iframeId;" + + " iframe.style = 'display: none; z-index: 0; position: absolute; width: 0px; height: 0px';" + + " document.body.append(iframe);" + + " }" + + " if (iframe.contentWindow.document.getElementById('" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_plugin_scripts') == null) {" + + " var script = iframe.contentWindow.document.createElement('script');" + + " script.id = '" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_plugin_scripts';" + + " script.innerHTML = " + PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED + ";" + + " iframe.contentWindow.document.body.append(script);" + + " }" + + " }" + + " clearInterval(interval);" + + " });" + + "})();"; + + private static final String CONTENT_WORLD_WRAPPER_JS_SOURCE = "(function() {" + + " var interval = setInterval(function() {" + + " if (document.body == null) {return;}" + + " var iframeId = '" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_" + PluginScriptsUtil.VAR_CONTENT_WORLD_NAME + "';" + + " var iframe = document.getElementById(iframeId);" + + " if (iframe == null) {" + + " iframe = document.createElement('iframe');" + + " iframe.id = iframeId;" + + " iframe.style = 'display: none; z-index: 0; position: absolute; width: 0px; height: 0px';" + + " document.body.append(iframe);" + + " }" + + " if (iframe.contentWindow.document.querySelector('#" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_plugin_scripts') == null) {" + + " return;" + + " }" + + " var script = iframe.contentWindow.document.createElement('script');" + + " script.innerHTML = " + PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED + ";" + + " iframe.contentWindow.document.body.append(script);" + + " clearInterval(interval);" + + " });" + + "})();"; + + private static final String DOCUMENT_READY_WRAPPER_JS_SOURCE = "if (document.readyState === 'interactive' || document.readyState === 'complete') { " + + " " + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE + + "}"; + + @Override + public void dispose() { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT) && contentWorldsCreatorScript != null) { + contentWorldsCreatorScript.remove(); + } + removeAllUserOnlyScripts(); + removeAllPluginScripts(); + webView = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScript.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScript.java new file mode 100644 index 0000000..1f8e560 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScript.java @@ -0,0 +1,129 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class UserScript { + @Nullable + private String groupName; + @NonNull + private String source; + @NonNull + private UserScriptInjectionTime injectionTime; + @NonNull + private ContentWorld contentWorld; + @NonNull + private Set allowedOriginRules = new HashSet<>(); + + public UserScript(@Nullable String groupName, @NonNull String source, + @NonNull UserScriptInjectionTime injectionTime, @Nullable ContentWorld contentWorld, + @Nullable Set allowedOriginRules) { + this.groupName = groupName; + this.source = source; + this.injectionTime = injectionTime; + this.contentWorld = contentWorld == null ? ContentWorld.PAGE : contentWorld; + this.allowedOriginRules = allowedOriginRules == null ? new HashSet() {{ + add("*"); + }} : allowedOriginRules; + } + + @Nullable + public static UserScript fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String groupName = (String) map.get("groupName"); + String source = (String) map.get("source"); + UserScriptInjectionTime injectionTime = UserScriptInjectionTime.fromValue((int) map.get("injectionTime")); + ContentWorld contentWorld = ContentWorld.fromMap((Map) map.get("contentWorld")); + Set allowedOriginRules = new HashSet<>((List) map.get("allowedOriginRules")); + assert source != null; + return new UserScript(groupName, source, injectionTime, contentWorld, allowedOriginRules); + } + + @Nullable + public String getGroupName() { + return groupName; + } + + public void setGroupName(@Nullable String groupName) { + this.groupName = groupName; + } + + @NonNull + public String getSource() { + return source; + } + + public void setSource(@NonNull String source) { + this.source = source; + } + + @NonNull + public UserScriptInjectionTime getInjectionTime() { + return injectionTime; + } + + public void setInjectionTime(@NonNull UserScriptInjectionTime injectionTime) { + this.injectionTime = injectionTime; + } + + @NonNull + public ContentWorld getContentWorld() { + return contentWorld; + } + + public void setContentWorld(@Nullable ContentWorld contentWorld) { + this.contentWorld = contentWorld == null ? ContentWorld.PAGE : contentWorld; + } + + @NonNull + public Set getAllowedOriginRules() { + return allowedOriginRules; + } + + public void setAllowedOriginRules(@NonNull Set allowedOriginRules) { + this.allowedOriginRules = allowedOriginRules; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserScript that = (UserScript) o; + + if (groupName != null ? !groupName.equals(that.groupName) : that.groupName != null) + return false; + if (!source.equals(that.source)) return false; + if (injectionTime != that.injectionTime) return false; + if (!contentWorld.equals(that.contentWorld)) return false; + return allowedOriginRules.equals(that.allowedOriginRules); + } + + @Override + public int hashCode() { + int result = groupName != null ? groupName.hashCode() : 0; + result = 31 * result + source.hashCode(); + result = 31 * result + injectionTime.hashCode(); + result = 31 * result + contentWorld.hashCode(); + result = 31 * result + allowedOriginRules.hashCode(); + return result; + } + + @Override + public String toString() { + return "UserScript{" + + "groupName='" + groupName + '\'' + + ", source='" + source + '\'' + + ", injectionTime=" + injectionTime + + ", contentWorld=" + contentWorld + + ", allowedOriginRules=" + allowedOriginRules + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScriptInjectionTime.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScriptInjectionTime.java new file mode 100644 index 0000000..bfceeb8 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScriptInjectionTime.java @@ -0,0 +1,28 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +public enum UserScriptInjectionTime { + AT_DOCUMENT_START (0), + AT_DOCUMENT_END (1); + + private final int value; + + private UserScriptInjectionTime(int value) { + this.value = value; + } + + public boolean equalsValue(int otherValue) { + return value == otherValue; + } + + public static UserScriptInjectionTime fromValue(int value) { + for( UserScriptInjectionTime type : UserScriptInjectionTime.values()) { + if(value == type.toValue()) + return type; + } + throw new IllegalArgumentException("No enum constant: " + value); + } + + public int toValue() { + return this.value; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessage.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessage.java new file mode 100644 index 0000000..21ed0f0 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessage.java @@ -0,0 +1,23 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.List; + +public class WebMessage { + @Nullable + public String data; + @Nullable + public List ports; + + public WebMessage(@Nullable String data, @Nullable List ports) { + this.data = data; + this.ports = ports; + } + + public void dispose() { + if (ports != null) { + ports.clear(); + } + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessageCompatExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessageCompatExt.java new file mode 100644 index 0000000..b635b3c --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessageCompatExt.java @@ -0,0 +1,117 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebViewFeature; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class WebMessageCompatExt { + @Nullable + private Object data; + private @WebMessageCompat.Type int type; + + @Nullable + private List ports; + + public WebMessageCompatExt(@Nullable Object data, int type, @Nullable List ports) { + this.data = data; + this.type = type; + this.ports = ports; + } + + public static WebMessageCompatExt fromMapWebMessageCompat(@NonNull WebMessageCompat message) { + Object data; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + data = message.getArrayBuffer(); + } else { + data = message.getData(); + } + return new WebMessageCompatExt(data, message.getType(), null); + } + + @Nullable + public static WebMessageCompatExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + Object data = map.get("data"); + Integer type = (Integer) map.get("type"); + List> portMapList = (List>) map.get("ports"); + List ports = null; + if (portMapList != null && !portMapList.isEmpty()) { + ports = new ArrayList<>(); + for (Map portMap : portMapList) { + ports.add(WebMessagePortCompatExt.fromMap(portMap)); + } + } + return new WebMessageCompatExt(data, type, ports); + } + + public Map toMap() { + Map proxyRuleMap = new HashMap<>(); + proxyRuleMap.put("data", data); + proxyRuleMap.put("type", type); + return proxyRuleMap; + } + + @Nullable + public Object getData() { + return data; + } + + public void setData(@Nullable Object data) { + this.data = data; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + @Nullable + public List getPorts() { + return ports; + } + + public void setPorts(@Nullable List ports) { + this.ports = ports; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebMessageCompatExt that = (WebMessageCompatExt) o; + + if (type != that.type) return false; + if (!Objects.equals(data, that.data)) return false; + return Objects.equals(ports, that.ports); + } + + @Override + public int hashCode() { + int result = data != null ? data.hashCode() : 0; + result = 31 * result + type; + result = 31 * result + (ports != null ? ports.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "WebMessageCompatExt{" + + "data=" + data + + ", type=" + type + + ", ports=" + ports + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePort.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePort.java new file mode 100644 index 0000000..a9b6cdf --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePort.java @@ -0,0 +1,136 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.text.TextUtils; +import android.webkit.ValueCallback; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageChannel; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; + +import java.util.ArrayList; +import java.util.List; + +public class WebMessagePort { + public String name; + @Nullable + public WebMessageChannel webMessageChannel; + public boolean isClosed = false; + public boolean isTransferred = false; + public boolean isStarted = false; + + public WebMessagePort(String name, @NonNull WebMessageChannel webMessageChannel) { + this.name = name; + this.webMessageChannel = webMessageChannel; + } + + public void setWebMessageCallback(final ValueCallback callback) throws Exception { + if (isClosed || isTransferred) { + throw new Exception("Port is already closed or transferred"); + } + this.isStarted = true; + InAppWebViewInterface webView = webMessageChannel != null && webMessageChannel.webView != null ? webMessageChannel.webView : null; + if (webView != null) { + int index = name.equals("port1") ? 0 : 1; + webView.evaluateJavascript("(function() {" + + " var webMessageChannel = " + JavaScriptBridgeJS.WEB_MESSAGE_CHANNELS_VARIABLE_NAME + "['" + webMessageChannel.id + "'];" + + " if (webMessageChannel != null) {" + + " webMessageChannel." + this.name + ".onmessage = function (event) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onWebMessagePortMessageReceived', {" + + " 'webMessageChannelId': '" + webMessageChannel.id + "'," + + " 'index': " + index + "," + + " 'message': event.data" + + " });" + + " }" + + " }" + + "})();", null, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (callback != null) { + callback.onReceiveValue(null); + } + } + }); + } else { + if (callback != null) { + callback.onReceiveValue(null); + } + } + } + + public void postMessage(WebMessage message, final ValueCallback callback) throws Exception { + if (isClosed || isTransferred) { + throw new Exception("Port is already closed or transferred"); + } + InAppWebViewInterface webView = webMessageChannel != null && webMessageChannel.webView != null ? webMessageChannel.webView : null; + if (webView != null) { + String portsString = "null"; + List ports = message.ports; + if (ports != null) { + List portArrayString = new ArrayList<>(); + for (WebMessagePort port : ports) { + if (port == this) { + throw new Exception("Source port cannot be transferred"); + } + if (port.isStarted) { + throw new Exception("Port is already started"); + } + if (port.isClosed || port.isTransferred) { + throw new Exception("Port is already closed or transferred"); + } + port.isTransferred = true; + portArrayString.add(JavaScriptBridgeJS.WEB_MESSAGE_CHANNELS_VARIABLE_NAME + "['" + webMessageChannel.id + "']." + port.name); + } + portsString = "[" + TextUtils.join(", ", portArrayString) + "]"; + } + String data = message.data != null ? Util.replaceAll(message.data, "\'", "\\'") : "null"; + String source = "(function() {" + + " var webMessageChannel = " + JavaScriptBridgeJS.WEB_MESSAGE_CHANNELS_VARIABLE_NAME + "['" + webMessageChannel.id + "'];" + + " if (webMessageChannel != null) {" + + " webMessageChannel." + this.name + ".postMessage('" + data + "', " + portsString + ");" + + " }" + + "})();"; + webView.evaluateJavascript(source, null, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + callback.onReceiveValue(null); + } + }); + } else { + callback.onReceiveValue(null); + } + message.dispose(); + } + + public void close(final ValueCallback callback) throws Exception { + if (isTransferred) { + throw new Exception("Port is already transferred"); + } + isClosed = true; + InAppWebViewInterface webView = webMessageChannel != null && webMessageChannel.webView != null ? webMessageChannel.webView : null; + if (webView != null) { + String source = "(function() {" + + " var webMessageChannel = " + JavaScriptBridgeJS.WEB_MESSAGE_CHANNELS_VARIABLE_NAME + "['" + webMessageChannel.id + "'];" + + " if (webMessageChannel != null) {" + + " webMessageChannel." + this.name + ".close();" + + " }" + + "})();"; + webView.evaluateJavascript(source, null, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + callback.onReceiveValue(null); + } + }); + } else { + callback.onReceiveValue(null); + } + } + + public void dispose() { + isClosed = true; + webMessageChannel = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePortCompatExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePortCompatExt.java new file mode 100644 index 0000000..a03e4d6 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePortCompatExt.java @@ -0,0 +1,78 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class WebMessagePortCompatExt { + private int index; + @NonNull + private String webMessageChannelId; + + public WebMessagePortCompatExt(int index, @NonNull String webMessageChannelId) { + this.index = index; + this.webMessageChannelId = webMessageChannelId; + } + + @Nullable + public static WebMessagePortCompatExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + Integer index = (Integer) map.get("index"); + String webMessageChannelId = (String) map.get("webMessageChannelId"); + return new WebMessagePortCompatExt(index, webMessageChannelId); + } + + public Map toMap() { + Map proxyRuleMap = new HashMap<>(); + proxyRuleMap.put("index", index); + proxyRuleMap.put("webMessageChannelId", webMessageChannelId); + return proxyRuleMap; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + @NonNull + public String getWebMessageChannelId() { + return webMessageChannelId; + } + + public void setWebMessageChannelId(@NonNull String webMessageChannelId) { + this.webMessageChannelId = webMessageChannelId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebMessagePortCompatExt that = (WebMessagePortCompatExt) o; + + if (index != that.index) return false; + return webMessageChannelId.equals(that.webMessageChannelId); + } + + @Override + public int hashCode() { + int result = index; + result = 31 * result + webMessageChannelId.hashCode(); + return result; + } + + @Override + public String toString() { + return "WebMessagePortCompatExt{" + + "index=" + index + + ", webMessageChannelId='" + webMessageChannelId + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceErrorExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceErrorExt.java new file mode 100644 index 0000000..bf23cc7 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceErrorExt.java @@ -0,0 +1,91 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.webkit.WebResourceError; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebResourceErrorCompat; +import androidx.webkit.WebViewFeature; + +import java.util.HashMap; +import java.util.Map; + +public class WebResourceErrorExt { + private int type; + @NonNull + private String description; + + public WebResourceErrorExt(int type, @NonNull String description) { + this.type = type; + this.description = description; + } + + @RequiresApi(Build.VERSION_CODES.M) + static public WebResourceErrorExt fromWebResourceError(@NonNull WebResourceError error) { + return new WebResourceErrorExt(error.getErrorCode(), error.getDescription().toString()); + } + + @RequiresApi(Build.VERSION_CODES.M) + static public WebResourceErrorExt fromWebResourceError(@NonNull WebResourceErrorCompat error) { + int type = -1; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) { + type = error.getErrorCode(); + } + String description = ""; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION)) { + description = error.getDescription().toString(); + } + return new WebResourceErrorExt(type, description); + } + + public Map toMap() { + Map webResourceErrorMap = new HashMap<>(); + webResourceErrorMap.put("type", getType()); + webResourceErrorMap.put("description", getDescription()); + return webResourceErrorMap; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + @NonNull + public String getDescription() { + return description; + } + + public void setDescription(@NonNull String description) { + this.description = description; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebResourceErrorExt that = (WebResourceErrorExt) o; + + if (type != that.type) return false; + return description.equals(that.description); + } + + @Override + public int hashCode() { + int result = type; + result = 31 * result + description.hashCode(); + return result; + } + + @Override + public String toString() { + return "WebResourceErrorExt{" + + "type=" + type + + ", description='" + description + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceRequestExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceRequestExt.java new file mode 100644 index 0000000..b33d807 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceRequestExt.java @@ -0,0 +1,146 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.webkit.WebResourceRequest; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebResourceRequestCompat; +import androidx.webkit.WebViewFeature; + +import java.util.HashMap; +import java.util.Map; + +public class WebResourceRequestExt { + @NonNull + private String url; + private Map headers; + private boolean isRedirect; + private boolean hasGesture; + private boolean isForMainFrame; + private String method; + + public WebResourceRequestExt(@NonNull String url, Map headers, boolean isRedirect, boolean hasGesture, boolean isForMainFrame, String method) { + this.url = url; + this.headers = headers; + this.isRedirect = isRedirect; + this.hasGesture = hasGesture; + this.isForMainFrame = isForMainFrame; + this.method = method; + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + static public WebResourceRequestExt fromWebResourceRequest(@NonNull WebResourceRequest request) { + boolean isRedirect = false; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT)) { + isRedirect = WebResourceRequestCompat.isRedirect(request); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + isRedirect = request.isRedirect(); + } + return new WebResourceRequestExt(request.getUrl().toString(), + request.getRequestHeaders(), + isRedirect, + request.hasGesture(), + request.isForMainFrame(), + request.getMethod() + ); + } + + public Map toMap() { + Map webResourceRequestMap = new HashMap<>(); + webResourceRequestMap.put("url", url); + webResourceRequestMap.put("headers", headers); + webResourceRequestMap.put("isRedirect", isRedirect); + webResourceRequestMap.put("hasGesture", hasGesture); + webResourceRequestMap.put("isForMainFrame", isForMainFrame); + webResourceRequestMap.put("method", method); + return webResourceRequestMap; + } + + @NonNull + public String getUrl() { + return url; + } + + public void setUrl(@NonNull String url) { + this.url = url; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public boolean isRedirect() { + return isRedirect; + } + + public void setRedirect(boolean redirect) { + isRedirect = redirect; + } + + public boolean isHasGesture() { + return hasGesture; + } + + public void setHasGesture(boolean hasGesture) { + this.hasGesture = hasGesture; + } + + public boolean isForMainFrame() { + return isForMainFrame; + } + + public void setForMainFrame(boolean forMainFrame) { + isForMainFrame = forMainFrame; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebResourceRequestExt that = (WebResourceRequestExt) o; + + if (isRedirect != that.isRedirect) return false; + if (hasGesture != that.hasGesture) return false; + if (isForMainFrame != that.isForMainFrame) return false; + if (!url.equals(that.url)) return false; + if (headers != null ? !headers.equals(that.headers) : that.headers != null) return false; + return method != null ? method.equals(that.method) : that.method == null; + } + + @Override + public int hashCode() { + int result = url.hashCode(); + result = 31 * result + (headers != null ? headers.hashCode() : 0); + result = 31 * result + (isRedirect ? 1 : 0); + result = 31 * result + (hasGesture ? 1 : 0); + result = 31 * result + (isForMainFrame ? 1 : 0); + result = 31 * result + (method != null ? method.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "WebResourceRequestExt{" + + "url=" + url + + ", headers=" + headers + + ", isRedirect=" + isRedirect + + ", hasGesture=" + hasGesture + + ", isForMainFrame=" + isForMainFrame + + ", method='" + method + '\'' + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceResponseExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceResponseExt.java new file mode 100644 index 0000000..824ccc0 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceResponseExt.java @@ -0,0 +1,177 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.webkit.WebResourceResponse; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class WebResourceResponseExt { + @Nullable + private String contentType; + @Nullable + private String contentEncoding; + @Nullable + private Integer statusCode; + @Nullable + private String reasonPhrase; + @Nullable + private Map headers; + @Nullable + private byte[] data; + + public WebResourceResponseExt(@Nullable String contentType, @Nullable String contentEncoding, @Nullable Integer statusCode, + @Nullable String reasonPhrase, @Nullable Map headers, @Nullable byte[] data) { + this.contentType = contentType; + this.contentEncoding = contentEncoding; + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + this.headers = headers; + this.data = data; + } + + static public WebResourceResponseExt fromWebResourceResponse(@NonNull WebResourceResponse response) { + Integer statusCode = null; + String reasonPhrase = null; + Map headers = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + statusCode = response.getStatusCode(); + reasonPhrase = response.getReasonPhrase(); + headers = response.getResponseHeaders(); + } + return new WebResourceResponseExt(response.getMimeType(), + response.getEncoding(), + statusCode, + reasonPhrase, + headers, + Util.readAllBytes(response.getData()) + ); + } + + @Nullable + public static WebResourceResponseExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String contentType = (String) map.get("contentType"); + String contentEncoding = (String) map.get("contentEncoding"); + Integer statusCode = (Integer) map.get("statusCode"); + String reasonPhrase = (String) map.get("reasonPhrase"); + Map headers = (Map) map.get("headers"); + byte[] data = (byte[]) map.get("data"); + return new WebResourceResponseExt(contentType, contentEncoding, statusCode, reasonPhrase, headers, data); + } + + public Map toMap() { + Map webResourceResponseMap = new HashMap<>(); + webResourceResponseMap.put("contentType", contentType); + webResourceResponseMap.put("contentEncoding", contentEncoding); + webResourceResponseMap.put("statusCode", statusCode); + webResourceResponseMap.put("reasonPhrase", reasonPhrase); + webResourceResponseMap.put("headers", headers); + webResourceResponseMap.put("data", data); + return webResourceResponseMap; + } + + @Nullable + public String getContentType() { + return contentType; + } + + public void setContentType(@Nullable String contentType) { + this.contentType = contentType; + } + + @Nullable + public String getContentEncoding() { + return contentEncoding; + } + + public void setContentEncoding(@Nullable String contentEncoding) { + this.contentEncoding = contentEncoding; + } + + @Nullable + public Integer getStatusCode() { + return statusCode; + } + + public void setStatusCode(@Nullable Integer statusCode) { + this.statusCode = statusCode; + } + + @Nullable + public String getReasonPhrase() { + return reasonPhrase; + } + + public void setReasonPhrase(@Nullable String reasonPhrase) { + this.reasonPhrase = reasonPhrase; + } + + @Nullable + public Map getHeaders() { + return headers; + } + + public void setHeaders(@Nullable Map headers) { + this.headers = headers; + } + + @Nullable + public byte[] getData() { + return data; + } + + public void setData(@Nullable byte[] data) { + this.data = data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebResourceResponseExt that = (WebResourceResponseExt) o; + + if (contentType != null ? !contentType.equals(that.contentType) : that.contentType != null) + return false; + if (contentEncoding != null ? !contentEncoding.equals(that.contentEncoding) : that.contentEncoding != null) + return false; + if (statusCode != null ? !statusCode.equals(that.statusCode) : that.statusCode != null) + return false; + if (reasonPhrase != null ? !reasonPhrase.equals(that.reasonPhrase) : that.reasonPhrase != null) + return false; + if (headers != null ? !headers.equals(that.headers) : that.headers != null) return false; + return Arrays.equals(data, that.data); + } + + @Override + public int hashCode() { + int result = contentType != null ? contentType.hashCode() : 0; + result = 31 * result + (contentEncoding != null ? contentEncoding.hashCode() : 0); + result = 31 * result + (statusCode != null ? statusCode.hashCode() : 0); + result = 31 * result + (reasonPhrase != null ? reasonPhrase.hashCode() : 0); + result = 31 * result + (headers != null ? headers.hashCode() : 0); + result = 31 * result + Arrays.hashCode(data); + return result; + } + + @Override + public String toString() { + return "WebResourceResponseExt{" + + "contentType='" + contentType + '\'' + + ", contentEncoding='" + contentEncoding + '\'' + + ", statusCode=" + statusCode + + ", reasonPhrase='" + reasonPhrase + '\'' + + ", headers=" + headers + + ", data=" + Arrays.toString(data) + + '}'; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebViewAssetLoaderExt.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebViewAssetLoaderExt.java new file mode 100644 index 0000000..0a0c747 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebViewAssetLoaderExt.java @@ -0,0 +1,209 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.content.Context; +import android.os.Build; +import android.util.Log; +import android.webkit.WebResourceResponse; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewAssetLoader; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.Util; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +public class WebViewAssetLoaderExt implements Disposable { + @Nullable + public WebViewAssetLoader loader; + @NonNull + public List customPathHandlers; + + public WebViewAssetLoaderExt(@Nullable WebViewAssetLoader loader, @NonNull List customPathHandlers) { + this.loader = loader; + this.customPathHandlers = customPathHandlers; + } + + @Nullable + public static WebViewAssetLoaderExt fromMap(@Nullable Map map, @NonNull InAppWebViewFlutterPlugin plugin, @NonNull Context context) { + if (map == null) { + return null; + } + WebViewAssetLoader.Builder builder = new WebViewAssetLoader.Builder(); + String domain = (String) map.get("domain"); + Boolean httpAllowed = (Boolean) map.get("httpAllowed"); + List> pathHandlers = (List>) map.get("pathHandlers"); + List customPathHandlers = new ArrayList<>(); + if (domain != null && !domain.isEmpty()) { + builder.setDomain(domain); + } + if (httpAllowed != null) { + builder.setHttpAllowed(httpAllowed); + } + if (pathHandlers != null) { + for (Map pathHandler : pathHandlers) { + String type = (String) pathHandler.get("type"); + String path = (String) pathHandler.get("path"); + if (type == null || path == null) { + continue; + } + switch (type) { + case "AssetsPathHandler": + WebViewAssetLoader.AssetsPathHandler assetsPathHandler = + new WebViewAssetLoader.AssetsPathHandler(context); + builder.addPathHandler(path, assetsPathHandler); + break; + case "InternalStoragePathHandler": + String directory = (String) pathHandler.get("directory"); + if (directory == null) { + continue; + } + File dir = new File(directory); + WebViewAssetLoader.InternalStoragePathHandler internalStoragePathHandler = + new WebViewAssetLoader.InternalStoragePathHandler(context, dir); + builder.addPathHandler(path, internalStoragePathHandler); + break; + case "ResourcesPathHandler": + WebViewAssetLoader.ResourcesPathHandler resourcesPathHandler = new WebViewAssetLoader.ResourcesPathHandler(context); + builder.addPathHandler(path, resourcesPathHandler); + break; + default: + String id = (String) pathHandler.get("id"); + if (id == null) { + continue; + } + PathHandlerExt customPathHandler = new PathHandlerExt(id, plugin); + builder.addPathHandler(path, customPathHandler); + customPathHandlers.add(customPathHandler); + break; + } + } + } + return new WebViewAssetLoaderExt(builder.build(), customPathHandlers); + } + + @Override + public void dispose() { + for (PathHandlerExt pathHandler : customPathHandlers) { + pathHandler.dispose(); + } + customPathHandlers.clear(); + } + + public static class PathHandlerExt implements WebViewAssetLoader.PathHandler, Disposable { + + protected static final String LOG_TAG = "PathHandlerExt"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_custompathhandler_"; + + @NonNull + public String id; + @Nullable + public PathHandlerExtChannelDelegate channelDelegate; + + public PathHandlerExt(@NonNull String id, @NonNull InAppWebViewFlutterPlugin plugin) { + this.id = id; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id); + this.channelDelegate = new PathHandlerExtChannelDelegate(this, channel); + } + + @Nullable + @Override + public WebResourceResponse handle(@NonNull String path) { + if (channelDelegate != null) { + WebResourceResponseExt response = null; + + try { + response = channelDelegate.handle(path); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + + if (response != null) { + String contentType = response.getContentType(); + String contentEncoding = response.getContentEncoding(); + byte[] data = response.getData(); + Map responseHeaders = response.getHeaders(); + Integer statusCode = response.getStatusCode(); + String reasonPhrase = response.getReasonPhrase(); + + ByteArrayInputStream inputStream = (data != null) ? new ByteArrayInputStream(data) : null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && statusCode != null && reasonPhrase != null) { + return new WebResourceResponse(contentType, contentEncoding, statusCode, reasonPhrase, responseHeaders, inputStream); + } else { + return new WebResourceResponse(contentType, contentEncoding, inputStream); + } + } + } + return null; + } + + @Override + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + } + } + + public static class PathHandlerExtChannelDelegate extends ChannelDelegateImpl { + + @Nullable + private PathHandlerExt pathHandler; + + public PathHandlerExtChannelDelegate(@NonNull PathHandlerExt pathHandler, @NonNull MethodChannel channel) { + super(channel); + this.pathHandler = pathHandler; + } + + public static class HandleCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return WebResourceResponseExt.fromMap((Map) obj); + } + } + + public void handle(String path, @NonNull HandleCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("path", path); + channel.invokeMethod("handle", obj, callback); + } + + public static class SyncHandleCallback extends SyncBaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return (new HandleCallback()).decodeResult(obj); + } + } + + @Nullable + public WebResourceResponseExt handle(String path) throws InterruptedException { + MethodChannel channel = getChannel(); + if (channel == null) return null; + final SyncHandleCallback callback = new SyncHandleCallback(); + Map obj = new HashMap<>(); + obj.put("path", path); + return Util.invokeMethodAndWaitResult(channel, "handle", obj, callback); + } + + @Override + public void dispose() { + super.dispose(); + pathHandler = null; + } + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/ContextMenuSettings.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/ContextMenuSettings.java new file mode 100644 index 0000000..fb0f194 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/ContextMenuSettings.java @@ -0,0 +1,49 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import androidx.annotation.NonNull; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; + +import java.util.HashMap; +import java.util.Map; + +public class ContextMenuSettings implements ISettings { + public static final String LOG_TAG = "ContextMenuOptions"; + + public Boolean hideDefaultSystemContextMenuItems = false; + + @NonNull + @Override + public ContextMenuSettings parse(@NonNull Map options) { + for (Map.Entry pair : options.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "hideDefaultSystemContextMenuItems": + hideDefaultSystemContextMenuItems = (Boolean) value; + break; + } + } + + return this; + } + + @NonNull + public Map toMap() { + Map options = new HashMap<>(); + options.put("hideDefaultSystemContextMenuItems", hideDefaultSystemContextMenuItems); + return options; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull Object obj) { + Map realOptions = toMap(); + return realOptions; + } + +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/FlutterWebViewFactory.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/FlutterWebViewFactory.java new file mode 100644 index 0000000..6acd91e --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/FlutterWebViewFactory.java @@ -0,0 +1,81 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview.HeadlessInAppWebView; +import com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview.HeadlessInAppWebViewManager; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.FlutterWebView; + +import java.util.HashMap; + +import io.flutter.plugin.common.StandardMessageCodec; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; + +public class FlutterWebViewFactory extends PlatformViewFactory { + public static final String VIEW_TYPE_ID = "com.pichillilorenzo/flutter_inappwebview"; + private final InAppWebViewFlutterPlugin plugin; + + public FlutterWebViewFactory(final InAppWebViewFlutterPlugin plugin) { + super(StandardMessageCodec.INSTANCE); + this.plugin = plugin; + } + + @Override + public PlatformView create(Context context, int id, Object args) { + HashMap params = (HashMap) args; + FlutterWebView flutterWebView = null; + Object viewId = id; + + String keepAliveId = (String) params.get("keepAliveId"); + String headlessWebViewId = (String) params.get("headlessWebViewId"); + + HeadlessInAppWebViewManager headlessInAppWebViewManager = plugin.headlessInAppWebViewManager; + if (headlessWebViewId != null && headlessInAppWebViewManager != null) { + HeadlessInAppWebView headlessInAppWebView = headlessInAppWebViewManager.webViews.get(headlessWebViewId); + if (headlessInAppWebView != null) { + flutterWebView = headlessInAppWebView.disposeAndGetFlutterWebView(); + if (flutterWebView != null) { + flutterWebView.keepAliveId = keepAliveId; + } + } + } + + InAppWebViewManager inAppWebViewManager = plugin.inAppWebViewManager; + if (keepAliveId != null && flutterWebView == null && inAppWebViewManager != null) { + flutterWebView = inAppWebViewManager.keepAliveWebViews.get(keepAliveId); + if (flutterWebView != null) { + // be sure to remove the view from the previous parent. + View view = flutterWebView.getView(); + if (view != null) { + ViewGroup parent = (ViewGroup) view.getParent(); + if (parent != null) { + parent.removeView(view); + } + } + } + } + + boolean shouldMakeInitialLoad = flutterWebView == null; + if (flutterWebView == null) { + if (keepAliveId != null) { + viewId = keepAliveId; + } + flutterWebView = new FlutterWebView(plugin, context, viewId, params); + } + + if (keepAliveId != null && inAppWebViewManager != null) { + inAppWebViewManager.keepAliveWebViews.put(keepAliveId, flutterWebView); + } + + if (shouldMakeInitialLoad) { + flutterWebView.makeInitialLoad(params); + } + + return flutterWebView; + } +} + diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewInterface.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewInterface.java new file mode 100644 index 0000000..ece3129 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewInterface.java @@ -0,0 +1,119 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import android.content.Context; +import android.net.Uri; +import android.net.http.SslCertificate; +import android.os.Looper; +import android.webkit.ValueCallback; +import android.webkit.WebMessage; +import android.webkit.WebView; + +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobSettings; +import com.pichillilorenzo.flutter_inappwebview_android.types.ContentWorld; +import com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserContentController; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebViewSettings; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageChannel; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageListener; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +public interface InAppWebViewInterface { + Context getContext(); + String getUrl(); + String getTitle(); + int getProgress(); + void loadUrl(URLRequest urlRequest); + void postUrl(String url, byte[] postData); + void loadDataWithBaseURL(String baseUrl, String data, + String mimeType, String encoding, String historyUrl); + void loadFile(String assetFilePath) throws IOException; + void evaluateJavascript(String source, ContentWorld contentWorld, ValueCallback resultCallback); + void injectJavascriptFileFromUrl(String urlFile, Map scriptHtmlTagAttributes); + void injectCSSCode(String source); + void injectCSSFileFromUrl(String urlFile, Map cssLinkHtmlTagAttributes); + void reload(); + void goBack(); + boolean canGoBack(); + void goForward(); + boolean canGoForward(); + void goBackOrForward(int steps); + boolean canGoBackOrForward(int steps); + void stopLoading(); + boolean isLoading(); + void takeScreenshot(Map screenshotConfiguration, MethodChannel.Result result); + void setSettings(InAppWebViewSettings newSettings, HashMap newSettingsMap); + Map getCustomSettings(); + HashMap getCopyBackForwardList(); + void clearAllCache(); + void clearSslPreferences(); + void findAllAsync(String find); + void findNext(boolean forward); + void clearMatches(); + void scrollTo(Integer x, Integer y, Boolean animated); + void scrollBy(Integer x, Integer y, Boolean animated); + void onPause(); + void onResume(); + void pauseTimers(); + void resumeTimers(); + @Nullable + String printCurrentPage(@Nullable PrintJobSettings settings); + int getContentHeight(); + void getContentHeight(ValueCallback callback); + void getContentWidth(ValueCallback callback); + void zoomBy(float zoomFactor); + String getOriginalUrl(); + void getSelectedText(ValueCallback callback); + WebView.HitTestResult getHitTestResult(); + void getHitTestResult(ValueCallback callback); + boolean pageDown(boolean bottom); + boolean pageUp(boolean top); + void saveWebArchive(String basename, boolean autoname, ValueCallback callback); + boolean zoomIn(); + boolean zoomOut(); + void clearFocus(); + Map requestFocusNodeHref(); + Map requestImageRef(); + int getScrollX(); + int getScrollY(); + SslCertificate getCertificate(); + void clearHistory(); + void callAsyncJavaScript(String functionBody, Map arguments, ContentWorld contentWorld, ValueCallback resultCallback); + void isSecureContext(final ValueCallback resultCallback); + WebMessageChannel createCompatWebMessageChannel(); + WebMessageChannel createWebMessageChannel(ValueCallback callback); + void postWebMessage(WebMessage message, Uri targetOrigin); + void postWebMessage(com.pichillilorenzo.flutter_inappwebview_android.types.WebMessage message, Uri targetOrigin, ValueCallback callback) throws Exception; + void addWebMessageListener(WebMessageListener webMessageListener) throws Exception; + boolean canScrollVertically(); + boolean canScrollHorizontally(); + float getZoomScale(); + void getZoomScale(ValueCallback callback); + Map getContextMenu(); + void setContextMenu(Map contextMenu); + InAppWebViewFlutterPlugin getPlugin(); + void setPlugin(InAppWebViewFlutterPlugin plugin); + InAppBrowserDelegate getInAppBrowserDelegate(); + void setInAppBrowserDelegate(InAppBrowserDelegate inAppBrowserDelegate); + UserContentController getUserContentController(); + void setUserContentController(UserContentController userContentController); + Map getWebMessageChannels(); + void setWebMessageChannels(Map webMessageChannels); + void disposeWebMessageChannels(); + void disposeWebMessageListeners(); + Looper getWebViewLooper(); + boolean isInFullscreen(); + void setInFullscreen(boolean inFullscreen); + @Nullable + WebViewChannelDelegate getChannelDelegate(); + void setChannelDelegate(@Nullable WebViewChannelDelegate eventWebViewChannelDelegate); +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewManager.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewManager.java new file mode 100644 index 0000000..623354a --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewManager.java @@ -0,0 +1,219 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.os.Build; +import android.os.Message; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.ValueCallback; +import android.webkit.WebSettings; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.FlutterWebView; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class InAppWebViewManager extends ChannelDelegateImpl { + protected static final String LOG_TAG = "InAppWebViewManager"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_manager"; + + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public final Map keepAliveWebViews = new HashMap<>(); + + public final Map windowWebViewMessages = new HashMap<>(); + public int windowAutoincrementId = 0; + + public InAppWebViewManager(final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.plugin = plugin; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) { + switch (call.method) { + case "getDefaultUserAgent": + if (plugin != null) { + result.success(WebSettings.getDefaultUserAgent(plugin.applicationContext)); + } else { + result.success(null); + } + break; + case "clearClientCertPreferences": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + WebView.clearClientCertPreferences(new Runnable() { + @Override + public void run() { + result.success(true); + } + }); + } else { + result.success(false); + } + break; + case "getSafeBrowsingPrivacyPolicyUrl": + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL)) { + result.success(WebViewCompat.getSafeBrowsingPrivacyPolicyUrl().toString()); + } else + result.success(null); + break; + case "setSafeBrowsingAllowlist": + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ALLOWLIST)) { + Set hosts = new HashSet<>((List) call.argument("hosts")); + WebViewCompat.setSafeBrowsingAllowlist(hosts, new ValueCallback() { + @Override + public void onReceiveValue(Boolean value) { + result.success(value); + } + }); + } + else if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_WHITELIST)) { + List hosts = (List) call.argument("hosts"); + WebViewCompat.setSafeBrowsingWhitelist(hosts, new ValueCallback() { + @Override + public void onReceiveValue(Boolean value) { + result.success(value); + } + }); + } else + result.success(false); + break; + case "getCurrentWebViewPackage": + { + Context context = null; + if (plugin != null) { + context = plugin.activity; + if (context == null) { + context = plugin.applicationContext; + } + } + PackageInfo packageInfo = context != null ? WebViewCompat.getCurrentWebViewPackage(context) : null; + result.success(packageInfo != null ? convertWebViewPackageToMap(packageInfo) : null); + } + break; + case "setWebContentsDebuggingEnabled": + { + boolean debuggingEnabled = (boolean) call.argument("debuggingEnabled"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + WebView.setWebContentsDebuggingEnabled(debuggingEnabled); + } + } + result.success(true); + break; + case "getVariationsHeader": + if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_VARIATIONS_HEADER)) { + result.success(WebViewCompat.getVariationsHeader()); + } + else { + result.success(null); + } + break; + case "isMultiProcessEnabled": + if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROCESS)) { + result.success(WebViewCompat.isMultiProcessEnabled()); + } + else { + result.success(false); + } + break; + case "disableWebView": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + WebView.disableWebView(); + } + result.success(true); + break; + case "disposeKeepAlive": + final String keepAliveId = (String) call.argument("keepAliveId"); + if (keepAliveId != null) { + disposeKeepAlive(keepAliveId); + } + result.success(true); + break; + case "clearAllCache": + { + Context context = null; + if (plugin != null) { + context = plugin.activity; + if (context == null) { + context = plugin.applicationContext; + } + if (context != null) { + boolean includeDiskFiles = (boolean) call.argument("includeDiskFiles"); + clearAllCache(context, includeDiskFiles); + } + } + } + result.success(true); + break; + default: + result.notImplemented(); + } + } + + @NonNull + public Map convertWebViewPackageToMap(@NonNull PackageInfo webViewPackageInfo) { + HashMap webViewPackageInfoMap = new HashMap<>(); + + webViewPackageInfoMap.put("versionName", webViewPackageInfo.versionName); + webViewPackageInfoMap.put("packageName", webViewPackageInfo.packageName); + + return webViewPackageInfoMap; + } + + public void disposeKeepAlive(@NonNull String keepAliveId) { + FlutterWebView flutterWebView = keepAliveWebViews.get(keepAliveId); + if (flutterWebView != null) { + flutterWebView.keepAliveId = null; + // be sure to remove the view from the previous parent. + View view = flutterWebView.getView(); + if (view != null) { + ViewGroup parent = (ViewGroup) view.getParent(); + if (parent != null) { + parent.removeView(view); + } + } + flutterWebView.dispose(); + } + if (keepAliveWebViews.containsKey(keepAliveId)) { + keepAliveWebViews.put(keepAliveId, null); + } + } + + public void clearAllCache(@NonNull Context context, boolean includeDiskFiles) { + WebView tempWebView = new WebView(context); + tempWebView.clearCache(includeDiskFiles); + tempWebView.destroy(); + } + + @Override + public void dispose() { + super.dispose(); + Collection flutterWebViews = keepAliveWebViews.values(); + for (FlutterWebView flutterWebView : flutterWebViews) { + String keepAliveId = flutterWebView.keepAliveId; + if (keepAliveId != null) { + disposeKeepAlive(flutterWebView.keepAliveId); + } + } + keepAliveWebViews.clear(); + windowWebViewMessages.clear(); + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/JavaScriptBridgeInterface.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/JavaScriptBridgeInterface.java new file mode 100644 index 0000000..615a1f8 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/JavaScriptBridgeInterface.java @@ -0,0 +1,173 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import android.os.Build; +import android.os.Handler; +import android.util.Log; +import android.webkit.JavascriptInterface; +import android.webkit.ValueCallback; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobController; +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobSettings; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class JavaScriptBridgeInterface { + private static final String LOG_TAG = "JSBridgeInterface"; + private InAppWebView inAppWebView; + + public JavaScriptBridgeInterface(InAppWebView inAppWebView) { + this.inAppWebView = inAppWebView; + } + + @JavascriptInterface + public void _hideContextMenu() { + if (inAppWebView == null) { + return; + } + + final Handler handler = new Handler(inAppWebView.getWebViewLooper()); + handler.post(new Runnable() { + @Override + public void run() { + if (inAppWebView != null && inAppWebView.floatingContextMenu != null) { + inAppWebView.hideContextMenu(); + } + } + }); + } + + @JavascriptInterface + public void _callHandler(final String handlerName, final String _callHandlerID, final String args) { + if (inAppWebView == null) { + return; + } + + // java.lang.RuntimeException: Methods marked with @UiThread must be executed on the main thread. + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/98 + final Handler handler = new Handler(inAppWebView.getWebViewLooper()); + handler.post(new Runnable() { + @Override + public void run() { + if (inAppWebView == null) { + // The webview has already been disposed, ignore. + return; + } + + if (handlerName.equals("onPrintRequest") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + PrintJobSettings settings = new PrintJobSettings(); + settings.handledByClient = true; + final String printJobId = inAppWebView.printCurrentPage(settings); + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onPrintRequest(inAppWebView.getUrl(), printJobId, new WebViewChannelDelegate.PrintRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull Boolean handledByClient) { + return !handledByClient; + } + + @Override + public void defaultBehaviour(@Nullable Boolean handledByClient) { + if (inAppWebView != null && inAppWebView.plugin != null && inAppWebView.plugin.printJobManager != null) { + PrintJobController printJobController = inAppWebView.plugin.printJobManager.jobs.get(printJobId); + if (printJobController != null) { + printJobController.disposeNoCancel(); + } + } + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }); + } + return; + } else if (handlerName.equals("callAsyncJavaScript")) { + try { + JSONArray arguments = new JSONArray(args); + JSONObject jsonObject = arguments.getJSONObject(0); + String resultUuid = jsonObject.getString("resultUuid"); + ValueCallback callAsyncJavaScriptCallback = inAppWebView.callAsyncJavaScriptCallbacks.get(resultUuid); + if (callAsyncJavaScriptCallback != null) { + callAsyncJavaScriptCallback.onReceiveValue(jsonObject.toString()); + inAppWebView.callAsyncJavaScriptCallbacks.remove(resultUuid); + } + } catch (JSONException e) { + Log.e(LOG_TAG, "", e); + } + return; + } else if (handlerName.equals("evaluateJavaScriptWithContentWorld")) { + try { + JSONArray arguments = new JSONArray(args); + JSONObject jsonObject = arguments.getJSONObject(0); + String resultUuid = jsonObject.getString("resultUuid"); + ValueCallback evaluateJavaScriptCallback = inAppWebView.evaluateJavaScriptContentWorldCallbacks.get(resultUuid); + if (evaluateJavaScriptCallback != null) { + evaluateJavaScriptCallback.onReceiveValue(jsonObject.has("value") ? jsonObject.get("value").toString() : "null"); + inAppWebView.evaluateJavaScriptContentWorldCallbacks.remove(resultUuid); + } + } catch (JSONException e) { + Log.e(LOG_TAG, "", e); + } + return; + } + + if (inAppWebView.channelDelegate != null) { + // invoke flutter javascript handler and send back flutter data as a JSON Object to javascript + inAppWebView.channelDelegate.onCallJsHandler(handlerName, args, new WebViewChannelDelegate.CallJsHandlerCallback() { + @Override + public void defaultBehaviour(@Nullable Object json) { + if (inAppWebView == null) { + // The webview has already been disposed, ignore. + return; + } + String sourceCode = "if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "] != null) { " + + "window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "].resolve(" + json + "); " + + "delete window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "]; " + + "}"; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + inAppWebView.evaluateJavascript(sourceCode, (ValueCallback) null); + } + else { + inAppWebView.loadUrl("javascript:" + sourceCode); + } + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + String message = errorCode + ((errorMessage != null) ? ", " + errorMessage : ""); + Log.e(LOG_TAG, message); + + if (inAppWebView == null) { + // The webview has already been disposed, ignore. + return; + } + + String sourceCode = "if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "] != null) { " + + "window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "].reject(new Error(" + JSONObject.quote(message) + ")); " + + "delete window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "]; " + + "}"; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + inAppWebView.evaluateJavascript(sourceCode, (ValueCallback) null); + } + else { + inAppWebView.loadUrl("javascript:" + sourceCode); + } + } + }); + } + } + }); + } + + public void dispose() { + inAppWebView = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/PlatformWebView.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/PlatformWebView.java new file mode 100644 index 0000000..93ae83f --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/PlatformWebView.java @@ -0,0 +1,9 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import java.util.HashMap; + +import io.flutter.plugin.platform.PlatformView; + +public interface PlatformWebView extends PlatformView { + void makeInitialLoad(HashMap params); +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegate.java new file mode 100644 index 0000000..5f87b93 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegate.java @@ -0,0 +1,1318 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import android.net.Uri; +import android.os.Build; +import android.webkit.ValueCallback; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebMessagePortCompat; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.find_interaction.FindInteractionChannelDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserActivity; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserSettings; +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobSettings; +import com.pichillilorenzo.flutter_inappwebview_android.types.BaseCallbackResultImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.ContentWorld; +import com.pichillilorenzo.flutter_inappwebview_android.types.CreateWindowAction; +import com.pichillilorenzo.flutter_inappwebview_android.types.CustomSchemeResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.DownloadStartRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.GeolocationPermissionShowPromptResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthenticationChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsAlertResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsBeforeUnloadResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsConfirmResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsPromptResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationAction; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationActionPolicy; +import com.pichillilorenzo.flutter_inappwebview_android.types.PermissionResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.SafeBrowsingResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.SslCertificateExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.SyncBaseCallbackResultImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessageCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessagePortCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceErrorExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceResponseExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebViewSettings; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageChannel; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageListener; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class WebViewChannelDelegate extends ChannelDelegateImpl { + static final String LOG_TAG = "WebViewChannelDelegate"; + + @Nullable + private InAppWebView webView; + + public WebViewChannelDelegate(@NonNull InAppWebView webView, @NonNull MethodChannel channel) { + super(channel); + this.webView = webView; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) { + WebViewChannelDelegateMethods method = null; + try { + method = WebViewChannelDelegateMethods.valueOf(call.method); + } catch (IllegalArgumentException e) { + result.notImplemented(); + return; + } + switch (method) { + case getUrl: + result.success((webView != null) ? webView.getUrl() : null); + break; + case getTitle: + result.success((webView != null) ? webView.getTitle() : null); + break; + case getProgress: + result.success((webView != null) ? webView.getProgress() : null); + break; + case loadUrl: + if (webView != null) { + Map urlRequest = (Map) call.argument("urlRequest"); + webView.loadUrl(URLRequest.fromMap(urlRequest)); + } + result.success(true); + break; + case postUrl: + if (webView != null) { + String url = (String) call.argument("url"); + byte[] postData = (byte[]) call.argument("postData"); + webView.postUrl(url, postData); + } + result.success(true); + break; + case loadData: + if (webView != null) { + String data = (String) call.argument("data"); + String mimeType = (String) call.argument("mimeType"); + String encoding = (String) call.argument("encoding"); + String baseUrl = (String) call.argument("baseUrl"); + String historyUrl = (String) call.argument("historyUrl"); + webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); + } + result.success(true); + break; + case loadFile: + if (webView != null) { + String assetFilePath = (String) call.argument("assetFilePath"); + try { + webView.loadFile(assetFilePath); + } catch (IOException e) { + e.printStackTrace(); + result.error(LOG_TAG, e.getMessage(), null); + return; + } + } + result.success(true); + break; + case evaluateJavascript: + if (webView != null) { + String source = (String) call.argument("source"); + Map contentWorldMap = (Map) call.argument("contentWorld"); + ContentWorld contentWorld = ContentWorld.fromMap(contentWorldMap); + webView.evaluateJavascript(source, contentWorld, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } else { + result.success(null); + } + break; + case injectJavascriptFileFromUrl: + if (webView != null) { + String urlFile = (String) call.argument("urlFile"); + Map scriptHtmlTagAttributes = (Map) call.argument("scriptHtmlTagAttributes"); + webView.injectJavascriptFileFromUrl(urlFile, scriptHtmlTagAttributes); + } + result.success(true); + break; + case injectCSSCode: + if (webView != null) { + String source = (String) call.argument("source"); + webView.injectCSSCode(source); + } + result.success(true); + break; + case injectCSSFileFromUrl: + if (webView != null) { + String urlFile = (String) call.argument("urlFile"); + Map cssLinkHtmlTagAttributes = (Map) call.argument("cssLinkHtmlTagAttributes"); + webView.injectCSSFileFromUrl(urlFile, cssLinkHtmlTagAttributes); + } + result.success(true); + break; + case reload: + if (webView != null) + webView.reload(); + result.success(true); + break; + case goBack: + if (webView != null) + webView.goBack(); + result.success(true); + break; + case canGoBack: + result.success((webView != null) && webView.canGoBack()); + break; + case goForward: + if (webView != null) + webView.goForward(); + result.success(true); + break; + case canGoForward: + result.success((webView != null) && webView.canGoForward()); + break; + case goBackOrForward: + if (webView != null) + webView.goBackOrForward((Integer) call.argument("steps")); + result.success(true); + break; + case canGoBackOrForward: + result.success((webView != null) && webView.canGoBackOrForward((Integer) call.argument("steps"))); + break; + case stopLoading: + if (webView != null) + webView.stopLoading(); + result.success(true); + break; + case isLoading: + result.success((webView != null) && webView.isLoading()); + break; + case takeScreenshot: + if (webView != null) { + Map screenshotConfiguration = (Map) call.argument("screenshotConfiguration"); + webView.takeScreenshot(screenshotConfiguration, result); + } else + result.success(null); + break; + case setSettings: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + InAppBrowserSettings inAppBrowserSettings = new InAppBrowserSettings(); + HashMap inAppBrowserSettingsMap = (HashMap) call.argument("settings"); + inAppBrowserSettings.parse(inAppBrowserSettingsMap); + inAppBrowserActivity.setSettings(inAppBrowserSettings, inAppBrowserSettingsMap); + } else if (webView != null) { + InAppWebViewSettings inAppWebViewSettings = new InAppWebViewSettings(); + HashMap inAppWebViewSettingsMap = (HashMap) call.argument("settings"); + inAppWebViewSettings.parse(inAppWebViewSettingsMap); + webView.setSettings(inAppWebViewSettings, inAppWebViewSettingsMap); + } + result.success(true); + break; + case getSettings: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + result.success(inAppBrowserActivity.getCustomSettings()); + } else { + result.success((webView != null) ? webView.getCustomSettings() : null); + } + break; + case close: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + inAppBrowserActivity.close(result); + } else { + result.notImplemented(); + } + break; + case show: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + inAppBrowserActivity.show(); + result.success(true); + } else { + result.notImplemented(); + } + break; + case hide: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + inAppBrowserActivity.hide(); + result.success(true); + } else { + result.notImplemented(); + } + break; + case isHidden: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + result.success(inAppBrowserActivity.isHidden); + } else { + result.notImplemented(); + } + break; + case getCopyBackForwardList: + result.success((webView != null) ? webView.getCopyBackForwardList() : null); + break; + case startSafeBrowsing: + if (webView != null && WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) { + WebViewCompat.startSafeBrowsing(webView.getContext(), new ValueCallback() { + @Override + public void onReceiveValue(Boolean success) { + result.success(success); + } + }); + } else { + result.success(false); + } + break; + case clearCache: + if (webView != null) + webView.clearAllCache(); + result.success(true); + break; + case clearSslPreferences: + if (webView != null) + webView.clearSslPreferences(); + result.success(true); + break; + case findAll: + if (webView != null) { + String find = (String) call.argument("find"); + webView.findAllAsync(find); + } + result.success(true); + break; + case findNext: + if (webView != null) { + Boolean forward = (Boolean) call.argument("forward"); + webView.findNext(forward); + } + result.success(true); + break; + case clearMatches: + if (webView != null) { + webView.clearMatches(); + } + result.success(true); + break; + case scrollTo: + if (webView != null) { + Integer x = (Integer) call.argument("x"); + Integer y = (Integer) call.argument("y"); + Boolean animated = (Boolean) call.argument("animated"); + webView.scrollTo(x, y, animated); + } + result.success(true); + break; + case scrollBy: + if (webView != null) { + Integer x = (Integer) call.argument("x"); + Integer y = (Integer) call.argument("y"); + Boolean animated = (Boolean) call.argument("animated"); + webView.scrollBy(x, y, animated); + } + result.success(true); + break; + case pause: + if (webView != null) { + webView.onPause(); + } + result.success(true); + break; + case resume: + if (webView != null) { + webView.onResume(); + } + result.success(true); + break; + case pauseTimers: + if (webView != null) { + webView.pauseTimers(); + } + result.success(true); + break; + case resumeTimers: + if (webView != null) { + webView.resumeTimers(); + } + result.success(true); + break; + case printCurrentPage: + if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + PrintJobSettings settings = new PrintJobSettings(); + Map settingsMap = (Map) call.argument("settings"); + if (settingsMap != null) { + settings.parse(settingsMap); + } + result.success(webView.printCurrentPage(settings)); + } else { + result.success(null); + } + break; + case getContentHeight: + if (webView instanceof InAppWebView) { + result.success(webView.getContentHeight()); + } else { + result.success(null); + } + break; + case getContentWidth: + if (webView instanceof InAppWebView) { + webView.getContentWidth(new ValueCallback() { + @Override + public void onReceiveValue(@Nullable Integer contentWidth) { + result.success(contentWidth); + } + }); + } else { + result.success(null); + } + break; + case zoomBy: + if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + double zoomFactor = (double) call.argument("zoomFactor"); + webView.zoomBy((float) zoomFactor); + } + result.success(true); + break; + case getOriginalUrl: + result.success((webView != null) ? webView.getOriginalUrl() : null); + break; + case getZoomScale: + if (webView instanceof InAppWebView) { + result.success(webView.getZoomScale()); + } else { + result.success(null); + } + break; + case getSelectedText: + if ((webView instanceof InAppWebView && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)) { + webView.getSelectedText(new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } else { + result.success(null); + } + break; + case getHitTestResult: + if (webView instanceof InAppWebView) { + result.success(HitTestResult.fromWebViewHitTestResult(webView.getHitTestResult()).toMap()); + } else { + result.success(null); + } + break; + case pageDown: + if (webView != null) { + boolean bottom = (boolean) call.argument("bottom"); + result.success(webView.pageDown(bottom)); + } else { + result.success(false); + } + break; + case pageUp: + if (webView != null) { + boolean top = (boolean) call.argument("top"); + result.success(webView.pageUp(top)); + } else { + result.success(false); + } + break; + case saveWebArchive: + if (webView != null) { + String filePath = (String) call.argument("filePath"); + boolean autoname = (boolean) call.argument("autoname"); + webView.saveWebArchive(filePath, autoname, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } else { + result.success(null); + } + break; + case zoomIn: + if (webView != null) { + result.success(webView.zoomIn()); + } else { + result.success(false); + } + break; + case zoomOut: + if (webView != null) { + result.success(webView.zoomOut()); + } else { + result.success(false); + } + break; + case clearFocus: + if (webView != null) { + webView.clearFocus(); + } + result.success(true); + break; + case setContextMenu: + if (webView != null) { + Map contextMenu = (Map) call.argument("contextMenu"); + webView.setContextMenu(contextMenu); + } + result.success(true); + break; + case requestFocusNodeHref: + if (webView != null) { + result.success(webView.requestFocusNodeHref()); + } else { + result.success(null); + } + break; + case requestImageRef: + if (webView != null) { + result.success(webView.requestImageRef()); + } else { + result.success(null); + } + break; + case getScrollX: + if (webView != null) { + result.success(webView.getScrollX()); + } else { + result.success(null); + } + break; + case getScrollY: + if (webView != null) { + result.success(webView.getScrollY()); + } else { + result.success(null); + } + break; + case getCertificate: + if (webView != null) { + result.success(SslCertificateExt.toMap(webView.getCertificate())); + } else { + result.success(null); + } + break; + case clearHistory: + if (webView != null) { + webView.clearHistory(); + } + result.success(true); + break; + case addUserScript: + if (webView != null && webView.getUserContentController() != null) { + Map userScriptMap = (Map) call.argument("userScript"); + UserScript userScript = UserScript.fromMap(userScriptMap); + result.success(webView.getUserContentController().addUserOnlyScript(userScript)); + } else { + result.success(false); + } + break; + case removeUserScript: + if (webView != null && webView.getUserContentController() != null) { + Integer index = (Integer) call.argument("index"); + Map userScriptMap = (Map) call.argument("userScript"); + UserScript userScript = UserScript.fromMap(userScriptMap); + result.success(webView.getUserContentController().removeUserOnlyScriptAt(index, userScript.getInjectionTime())); + } else { + result.success(false); + } + break; + case removeUserScriptsByGroupName: + if (webView != null && webView.getUserContentController() != null) { + String groupName = (String) call.argument("groupName"); + webView.getUserContentController().removeUserOnlyScriptsByGroupName(groupName); + } + result.success(true); + break; + case removeAllUserScripts: + if (webView != null && webView.getUserContentController() != null) { + webView.getUserContentController().removeAllUserOnlyScripts(); + } + result.success(true); + break; + case callAsyncJavaScript: + if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + String functionBody = (String) call.argument("functionBody"); + Map functionArguments = (Map) call.argument("arguments"); + Map contentWorldMap = (Map) call.argument("contentWorld"); + ContentWorld contentWorld = ContentWorld.fromMap(contentWorldMap); + webView.callAsyncJavaScript(functionBody, functionArguments, contentWorld, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } else { + result.success(null); + } + break; + case isSecureContext: + if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + webView.isSecureContext(new ValueCallback() { + @Override + public void onReceiveValue(Boolean value) { + result.success(value); + } + }); + } else { + result.success(false); + } + break; + case createWebMessageChannel: + if (webView != null) { + if (webView instanceof InAppWebView && WebViewFeature.isFeatureSupported(WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)) { + result.success(webView.createCompatWebMessageChannel().toMap()); + } else { + result.success(null); + } + } else { + result.success(null); + } + break; + case postWebMessage: + if (webView != null && WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)) { + WebMessageCompatExt message = WebMessageCompatExt.fromMap((Map) call.argument("message")); + String targetOrigin = (String) call.argument("targetOrigin"); + List compatPorts = new ArrayList<>(); + List portsExt = message.getPorts(); + if (portsExt != null) { + for (WebMessagePortCompatExt portExt : portsExt) { + WebMessageChannel webMessageChannel = webView.getWebMessageChannels().get(portExt.getWebMessageChannelId()); + if (webMessageChannel != null) { + if (webView instanceof InAppWebView) { + compatPorts.add(webMessageChannel.compatPorts.get(portExt.getIndex())); + } + } + } + } + Object data = message.getData(); + if (webView instanceof InAppWebView) { + try { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && data != null && + message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + WebViewCompat.postWebMessage((WebView) webView, + new WebMessageCompat((byte[]) data, compatPorts.toArray(new WebMessagePortCompat[0])), + Uri.parse(targetOrigin)); + } else { + WebViewCompat.postWebMessage((WebView) webView, + new WebMessageCompat(data != null ? data.toString() : null, compatPorts.toArray(new WebMessagePortCompat[0])), + Uri.parse(targetOrigin)); + } + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } + } else { + result.success(true); + } + break; + case addWebMessageListener: + if (webView != null) { + Map webMessageListenerMap = (Map) call.argument("webMessageListener"); + WebMessageListener webMessageListener = WebMessageListener.fromMap(webView, webView.getPlugin().messenger, webMessageListenerMap); + if (webView instanceof InAppWebView && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + try { + webView.addWebMessageListener(webMessageListener); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + } else { + result.success(true); + } + break; + case canScrollVertically: + if (webView != null) { + result.success(webView.canScrollVertically()); + } else { + result.success(false); + } + break; + case canScrollHorizontally: + if (webView != null) { + result.success(webView.canScrollHorizontally()); + } else { + result.success(false); + } + break; + case isInFullscreen: + if (webView != null) { + result.success(webView.isInFullscreen()); + } else { + result.success(false); + } + break; + case clearFormData: + if (webView != null) { + webView.clearFormData(); + } + result.success(true); + } + } + + /** + * @deprecated Use {@link FindInteractionChannelDelegate#onFindResultReceived} instead. + */ + @Deprecated + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("activeMatchOrdinal", activeMatchOrdinal); + obj.put("numberOfMatches", numberOfMatches); + obj.put("isDoneCounting", isDoneCounting); + channel.invokeMethod("onFindResultReceived", obj); + } + + public void onLongPressHitTestResult(HitTestResult hitTestResult) { + MethodChannel channel = getChannel(); + if (channel == null) return; + channel.invokeMethod("onLongPressHitTestResult", hitTestResult.toMap()); + } + + public void onScrollChanged(int x, int y) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("x", x); + obj.put("y", y); + channel.invokeMethod("onScrollChanged", obj); + } + + public void onDownloadStartRequest(DownloadStartRequest downloadStartRequest) { + MethodChannel channel = getChannel(); + if (channel == null) return; + channel.invokeMethod("onDownloadStartRequest", downloadStartRequest.toMap()); + } + + public void onCreateContextMenu(HitTestResult hitTestResult) { + MethodChannel channel = getChannel(); + if (channel == null) return; + channel.invokeMethod("onCreateContextMenu", hitTestResult.toMap()); + } + + public void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("x", scrollX); + obj.put("y", scrollY); + obj.put("clampedX", clampedX); + obj.put("clampedY", clampedY); + channel.invokeMethod("onOverScrolled", obj); + } + + public void onContextMenuActionItemClicked(int itemId, String itemTitle) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("id", itemId); + obj.put("androidId", itemId); + obj.put("iosId", null); + obj.put("title", itemTitle); + channel.invokeMethod("onContextMenuActionItemClicked", obj); + } + + public void onHideContextMenu() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onHideContextMenu", obj); + } + + public void onEnterFullscreen() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onEnterFullscreen", obj); + } + + public void onExitFullscreen() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onExitFullscreen", obj); + } + + public static class JsAlertCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public JsAlertResponse decodeResult(@Nullable Object obj) { + return JsAlertResponse.fromMap((Map) obj); + } + } + + public void onJsAlert(String url, String message, Boolean isMainFrame, @NonNull JsAlertCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("message", message); + obj.put("isMainFrame", isMainFrame); + channel.invokeMethod("onJsAlert", obj, callback); + } + + public static class JsConfirmCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public JsConfirmResponse decodeResult(@Nullable Object obj) { + return JsConfirmResponse.fromMap((Map) obj); + } + } + + public void onJsConfirm(String url, String message, Boolean isMainFrame, @NonNull JsConfirmCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("message", message); + obj.put("isMainFrame", isMainFrame); + channel.invokeMethod("onJsConfirm", obj, callback); + } + + public static class JsPromptCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public JsPromptResponse decodeResult(@Nullable Object obj) { + return JsPromptResponse.fromMap((Map) obj); + } + } + + public void onJsPrompt(String url, String message, String defaultValue, Boolean isMainFrame, @NonNull JsPromptCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("message", message); + obj.put("defaultValue", defaultValue); + obj.put("isMainFrame", isMainFrame); + channel.invokeMethod("onJsPrompt", obj, callback); + } + + public static class JsBeforeUnloadCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public JsBeforeUnloadResponse decodeResult(@Nullable Object obj) { + return JsBeforeUnloadResponse.fromMap((Map) obj); + } + } + + public void onJsBeforeUnload(String url, String message, @NonNull JsBeforeUnloadCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("message", message); + channel.invokeMethod("onJsBeforeUnload", obj, callback); + } + + public static class CreateWindowCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Boolean decodeResult(@Nullable Object obj) { + return (obj instanceof Boolean) && (boolean) obj; + } + } + + public void onCreateWindow(CreateWindowAction createWindowAction, @NonNull CreateWindowCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("onCreateWindow", createWindowAction.toMap(), callback); + } + + public void onCloseWindow() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onCloseWindow", obj); + } + + public static class GeolocationPermissionsShowPromptCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public GeolocationPermissionShowPromptResponse decodeResult(@Nullable Object obj) { + return GeolocationPermissionShowPromptResponse.fromMap((Map) obj); + } + } + + public void onGeolocationPermissionsShowPrompt(String origin, @NonNull GeolocationPermissionsShowPromptCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("origin", origin); + channel.invokeMethod("onGeolocationPermissionsShowPrompt", obj, callback); + } + + public void onGeolocationPermissionsHidePrompt() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onGeolocationPermissionsHidePrompt", obj); + } + + public void onConsoleMessage(String message, int messageLevel) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("message", message); + obj.put("messageLevel", messageLevel); + channel.invokeMethod("onConsoleMessage", obj); + } + + public void onProgressChanged(int progress) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("progress", progress); + channel.invokeMethod("onProgressChanged", obj); + } + + public void onTitleChanged(String title) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("title", title); + channel.invokeMethod("onTitleChanged", obj); + } + + public void onReceivedIcon(byte[] icon) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("icon", icon); + channel.invokeMethod("onReceivedIcon", obj); + } + + public void onReceivedTouchIconUrl(String url, boolean precomposed) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("precomposed", precomposed); + channel.invokeMethod("onReceivedTouchIconUrl", obj); + } + + public static class PermissionRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public PermissionResponse decodeResult(@Nullable Object obj) { + return PermissionResponse.fromMap((Map) obj); + } + } + + public void onPermissionRequest(String origin, List resources, Object frame, @NonNull PermissionRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("origin", origin); + obj.put("resources", resources); + obj.put("frame", frame); + channel.invokeMethod("onPermissionRequest", obj, callback); + } + + public void onPermissionRequestCanceled(String origin, List resources) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("origin", origin); + obj.put("resources", resources); + channel.invokeMethod("onPermissionRequestCanceled", obj); + } + + public static class ShouldOverrideUrlLoadingCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public NavigationActionPolicy decodeResult(@Nullable Object obj) { + Integer action = obj instanceof Integer ? (Integer) obj : NavigationActionPolicy.CANCEL.rawValue(); + return NavigationActionPolicy.fromValue(action); + } + } + + public void shouldOverrideUrlLoading(NavigationAction navigationAction, @NonNull ShouldOverrideUrlLoadingCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("shouldOverrideUrlLoading", navigationAction.toMap(), callback); + } + + public void onLoadStart(String url) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onLoadStart", obj); + } + + public void onLoadStop(String url) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onLoadStop", obj); + } + + public void onUpdateVisitedHistory(String url, boolean isReload) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("isReload", isReload); + channel.invokeMethod("onUpdateVisitedHistory", obj); + } + + public void onReceivedError(WebResourceRequestExt request, WebResourceErrorExt error) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("request", request.toMap()); + obj.put("error", error.toMap()); + channel.invokeMethod("onReceivedError", obj); + } + + public void onReceivedHttpError(WebResourceRequestExt request, WebResourceResponseExt errorResponse) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("request", request.toMap()); + obj.put("errorResponse", errorResponse.toMap()); + channel.invokeMethod("onReceivedHttpError", obj); + } + + public static class ReceivedHttpAuthRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public HttpAuthResponse decodeResult(@Nullable Object obj) { + return HttpAuthResponse.fromMap((Map) obj); + } + } + + public void onReceivedHttpAuthRequest(HttpAuthenticationChallenge challenge, @NonNull ReceivedHttpAuthRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("onReceivedHttpAuthRequest", challenge.toMap(), callback); + } + + public static class ReceivedServerTrustAuthRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public ServerTrustAuthResponse decodeResult(@Nullable Object obj) { + return ServerTrustAuthResponse.fromMap((Map) obj); + } + } + + public void onReceivedServerTrustAuthRequest(ServerTrustChallenge challenge, @NonNull ReceivedServerTrustAuthRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("onReceivedServerTrustAuthRequest", challenge.toMap(), callback); + } + + public static class ReceivedClientCertRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public ClientCertResponse decodeResult(@Nullable Object obj) { + return ClientCertResponse.fromMap((Map) obj); + } + } + + public void onReceivedClientCertRequest(ClientCertChallenge challenge, @NonNull ReceivedClientCertRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("onReceivedClientCertRequest", challenge.toMap(), callback); + } + + public void onZoomScaleChanged(float oldScale, float newScale) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("oldScale", oldScale); + obj.put("newScale", newScale); + channel.invokeMethod("onZoomScaleChanged", obj); + } + + public static class SafeBrowsingHitCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public SafeBrowsingResponse decodeResult(@Nullable Object obj) { + return SafeBrowsingResponse.fromMap((Map) obj); + } + } + + public void onSafeBrowsingHit(String url, int threatType, @NonNull SafeBrowsingHitCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("threatType", threatType); + channel.invokeMethod("onSafeBrowsingHit", obj, callback); + } + + public static class FormResubmissionCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Integer decodeResult(@Nullable Object obj) { + return obj instanceof Integer ? (Integer) obj : null; + } + } + + public void onFormResubmission(String url, @NonNull FormResubmissionCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onFormResubmission", obj, callback); + } + + public void onPageCommitVisible(String url) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onPageCommitVisible", obj); + } + + public void onRenderProcessGone(boolean didCrash, int rendererPriorityAtExit) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("didCrash", didCrash); + obj.put("rendererPriorityAtExit", rendererPriorityAtExit); + channel.invokeMethod("onRenderProcessGone", obj); + } + + public void onReceivedLoginRequest(String realm, String account, String args) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("realm", realm); + obj.put("account", account); + obj.put("args", args); + channel.invokeMethod("onReceivedLoginRequest", obj); + } + + public static class LoadResourceWithCustomSchemeCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public CustomSchemeResponse decodeResult(@Nullable Object obj) { + return CustomSchemeResponse.fromMap((Map) obj); + } + } + + public void onLoadResourceWithCustomScheme(WebResourceRequestExt request, @NonNull LoadResourceWithCustomSchemeCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("request", request.toMap()); + channel.invokeMethod("onLoadResourceWithCustomScheme", obj, callback); + } + + public static class SyncLoadResourceWithCustomSchemeCallback extends SyncBaseCallbackResultImpl { + @Nullable + @Override + public CustomSchemeResponse decodeResult(@Nullable Object obj) { + return (new LoadResourceWithCustomSchemeCallback()).decodeResult(obj); + } + } + + @Nullable + public CustomSchemeResponse onLoadResourceWithCustomScheme(WebResourceRequestExt request) throws InterruptedException { + MethodChannel channel = getChannel(); + if (channel == null) return null; + final Map obj = new HashMap<>(); + obj.put("request", request.toMap()); + final SyncLoadResourceWithCustomSchemeCallback callback = new SyncLoadResourceWithCustomSchemeCallback(); + return Util.invokeMethodAndWaitResult(channel, "onLoadResourceWithCustomScheme", obj, callback); + } + + public static class ShouldInterceptRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return WebResourceResponseExt.fromMap((Map) obj); + } + } + + public void shouldInterceptRequest(WebResourceRequestExt request, @NonNull ShouldInterceptRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("shouldInterceptRequest", request.toMap(), callback); + } + + public static class SyncShouldInterceptRequestCallback extends SyncBaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return (new ShouldInterceptRequestCallback()).decodeResult(obj); + } + } + + @Nullable + public WebResourceResponseExt shouldInterceptRequest(WebResourceRequestExt request) throws InterruptedException { + MethodChannel channel = getChannel(); + if (channel == null) return null; + final SyncShouldInterceptRequestCallback callback = new SyncShouldInterceptRequestCallback(); + return Util.invokeMethodAndWaitResult(channel, "shouldInterceptRequest", request.toMap(), callback); + } + + public static class RenderProcessUnresponsiveCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Integer decodeResult(@Nullable Object obj) { + return obj instanceof Integer ? (Integer) obj : null; + } + } + + public void onRenderProcessUnresponsive(String url, @NonNull RenderProcessUnresponsiveCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onRenderProcessUnresponsive", obj, callback); + } + + public static class RenderProcessResponsiveCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Integer decodeResult(@Nullable Object obj) { + return obj instanceof Integer ? (Integer) obj : null; + } + } + + public void onRenderProcessResponsive(String url, @NonNull RenderProcessResponsiveCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onRenderProcessResponsive", obj, callback); + } + + public static class CallJsHandlerCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Object decodeResult(@Nullable Object obj) { + return obj; + } + } + + public void onCallJsHandler(String handlerName, String args, @NonNull CallJsHandlerCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("handlerName", handlerName); + obj.put("args", args); + channel.invokeMethod("onCallJsHandler", obj, callback); + } + + public static class PrintRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Boolean decodeResult(@Nullable Object obj) { + return (obj instanceof Boolean) && (boolean) obj; + } + } + + public void onPrintRequest(String url, String printJobId, @NonNull PrintRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("printJobId", printJobId); + channel.invokeMethod("onPrintRequest", obj, callback); + } + + public void onRequestFocus() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onRequestFocus", obj); + } + + @Override + public void dispose() { + super.dispose(); + webView = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegateMethods.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegateMethods.java new file mode 100644 index 0000000..3c6d6ae --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegateMethods.java @@ -0,0 +1,86 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +public enum WebViewChannelDelegateMethods { + getUrl, + getTitle, + getProgress, + loadUrl, + postUrl, + loadData, + loadFile, + evaluateJavascript, + injectJavascriptFileFromUrl, + injectCSSCode, + injectCSSFileFromUrl, + reload, + goBack, + canGoBack, + goForward, + canGoForward, + goBackOrForward, + canGoBackOrForward, + stopLoading, + isLoading, + takeScreenshot, + setSettings, + getSettings, + close, + show, + hide, + isHidden, + getCopyBackForwardList, + startSafeBrowsing, + /** + * @deprecated + */ + @Deprecated + clearCache, + clearSslPreferences, + /** + * @deprecated + */ + @Deprecated + findAll, + findNext, + clearMatches, + scrollTo, + scrollBy, + pause, + resume, + pauseTimers, + resumeTimers, + printCurrentPage, + getContentHeight, + getContentWidth, + zoomBy, + getOriginalUrl, + getZoomScale, + getSelectedText, + getHitTestResult, + pageDown, + pageUp, + saveWebArchive, + zoomIn, + zoomOut, + clearFocus, + setContextMenu, + requestFocusNodeHref, + requestImageRef, + getScrollX, + getScrollY, + getCertificate, + clearHistory, + addUserScript, + removeUserScript, + removeUserScriptsByGroupName, + removeAllUserScripts, + callAsyncJavaScript, + isSecureContext, + createWebMessageChannel, + postWebMessage, + addWebMessageListener, + canScrollVertically, + canScrollHorizontally, + isInFullscreen, + clearFormData +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/DisplayListenerProxy.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/DisplayListenerProxy.java new file mode 100644 index 0000000..ebe487e --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/DisplayListenerProxy.java @@ -0,0 +1,122 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import static android.hardware.display.DisplayManager.DisplayListener; + +import android.annotation.TargetApi; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.util.Log; +import java.lang.reflect.Field; +import java.util.ArrayList; + +/** + * Works around an Android WebView bug by filtering some DisplayListener invocations. + * https://github.com/flutter/plugins/blob/master/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java + */ +@TargetApi(Build.VERSION_CODES.KITKAT) +public +class DisplayListenerProxy { + private static final String TAG = "DisplayListenerProxy"; + + private ArrayList listenersBeforeWebView; + + /** Should be called prior to the webview's initialization. */ + public void onPreWebViewInitialization(DisplayManager displayManager) { + listenersBeforeWebView = yoinkDisplayListeners(displayManager); + } + + /** Should be called after the webview's initialization. */ + public void onPostWebViewInitialization(final DisplayManager displayManager) { + final ArrayList webViewListeners = yoinkDisplayListeners(displayManager); + // We recorded the list of listeners prior to initializing webview, any new listeners we see + // after initializing the webview are listeners added by the webview. + webViewListeners.removeAll(listenersBeforeWebView); + + if (webViewListeners.isEmpty()) { + // The Android WebView registers a single display listener per process (even if there + // are multiple WebView instances) so this list is expected to be non-empty only the + // first time a webview is initialized. + // Note that in an add2app scenario if the application had instantiated a non Flutter + // WebView prior to instantiating the Flutter WebView we are not able to get a reference + // to the WebView's display listener and can't work around the bug. + // + // This means that webview resizes in add2app Flutter apps with a non Flutter WebView + // running on a system with a webview prior to 58.0.3029.125 may crash (the Android's + // behavior seems to be racy so it doesn't always happen). + return; + } + + for (DisplayListener webViewListener : webViewListeners) { + // Note that while DisplayManager.unregisterDisplayListener throws when given an + // unregistered listener, this isn't an issue as the WebView code never calls + // unregisterDisplayListener. + displayManager.unregisterDisplayListener(webViewListener); + + // We never explicitly unregister this listener as the webview's listener is never + // unregistered (it's released when the process is terminated). + displayManager.registerDisplayListener( + new DisplayListener() { + @Override + public void onDisplayAdded(int displayId) { + for (DisplayListener webViewListener : webViewListeners) { + webViewListener.onDisplayAdded(displayId); + } + } + + @Override + public void onDisplayRemoved(int displayId) { + for (DisplayListener webViewListener : webViewListeners) { + webViewListener.onDisplayRemoved(displayId); + } + } + + @Override + public void onDisplayChanged(int displayId) { + if (displayManager.getDisplay(displayId) == null) { + return; + } + for (DisplayListener webViewListener : webViewListeners) { + webViewListener.onDisplayChanged(displayId); + } + } + }, + null); + } + } + + @SuppressWarnings({"unchecked", "PrivateApi"}) + private static ArrayList yoinkDisplayListeners(DisplayManager displayManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // We cannot use reflection on Android P, but it shouldn't matter as it shipped + // with WebView 66.0.3359.158 and the WebView version the bug this code is working around was + // fixed in 61.0.3116.0. + return new ArrayList<>(); + } + try { + Field displayManagerGlobalField = DisplayManager.class.getDeclaredField("mGlobal"); + displayManagerGlobalField.setAccessible(true); + Object displayManagerGlobal = displayManagerGlobalField.get(displayManager); + Field displayListenersField = + displayManagerGlobal.getClass().getDeclaredField("mDisplayListeners"); + displayListenersField.setAccessible(true); + ArrayList delegates = + (ArrayList) displayListenersField.get(displayManagerGlobal); + + Field listenerField = null; + ArrayList listeners = new ArrayList<>(); + for (Object delegate : delegates) { + if (listenerField == null) { + listenerField = delegate.getClass().getField("mListener"); + listenerField.setAccessible(true); + } + DisplayManager.DisplayListener listener = + (DisplayManager.DisplayListener) listenerField.get(delegate); + listeners.add(listener); + } + return listeners; + } catch (NoSuchFieldException | IllegalAccessException e) { + Log.w(TAG, "Could not extract WebView's display listeners. " + e); + return new ArrayList<>(); + } + } +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/FlutterWebView.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/FlutterWebView.java new file mode 100644 index 0000000..5605b68 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/FlutterWebView.java @@ -0,0 +1,189 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Message; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.find_interaction.FindInteractionController; +import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshLayout; +import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshSettings; +import com.pichillilorenzo.flutter_inappwebview_android.webview.PlatformWebView; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScript; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FlutterWebView implements PlatformWebView { + + static final String LOG_TAG = "IAWFlutterWebView"; + + @Nullable + public InAppWebView webView; + @Nullable + public PullToRefreshLayout pullToRefreshLayout; + @Nullable + public String keepAliveId; + + public FlutterWebView(final InAppWebViewFlutterPlugin plugin, final Context context, Object id, + HashMap params) { + DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); + DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + displayListenerProxy.onPreWebViewInitialization(displayManager); + + keepAliveId = (String) params.get("keepAliveId"); + + Map initialSettings = (Map) params.get("initialSettings"); + Map contextMenu = (Map) params.get("contextMenu"); + Integer windowId = (Integer) params.get("windowId"); + List> initialUserScripts = (List>) params.get("initialUserScripts"); + Map pullToRefreshInitialSettings = (Map) params.get("pullToRefreshSettings"); + + InAppWebViewSettings customSettings = new InAppWebViewSettings(); + customSettings.parse(initialSettings); + + List userScripts = new ArrayList<>(); + if (initialUserScripts != null) { + for (Map initialUserScript : initialUserScripts) { + userScripts.add(UserScript.fromMap(initialUserScript)); + } + } + + webView = new InAppWebView(context, plugin, id, windowId, customSettings, contextMenu, + customSettings.useHybridComposition ? null : plugin.flutterView, userScripts); + displayListenerProxy.onPostWebViewInitialization(displayManager); + + // set MATCH_PARENT layout params to the WebView, otherwise it won't take all the available space! + webView.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + PullToRefreshSettings pullToRefreshSettings = new PullToRefreshSettings(); + pullToRefreshSettings.parse(pullToRefreshInitialSettings); + pullToRefreshLayout = new PullToRefreshLayout(context, plugin, id, pullToRefreshSettings); + pullToRefreshLayout.addView(webView); + pullToRefreshLayout.prepare(); + + FindInteractionController findInteractionController = new FindInteractionController(webView, plugin, id, null); + webView.findInteractionController = findInteractionController; + findInteractionController.prepare(); + + webView.prepare(); + } + + @Override + public View getView() { + return pullToRefreshLayout != null ? pullToRefreshLayout : webView; + } + + @SuppressLint("RestrictedApi") + public void makeInitialLoad(HashMap params) { + if (webView == null) { + return; + } + + Integer windowId = (Integer) params.get("windowId"); + Map initialUrlRequest = (Map) params.get("initialUrlRequest"); + final String initialFile = (String) params.get("initialFile"); + final Map initialData = (Map) params.get("initialData"); + + if (windowId != null) { + if (webView.plugin != null && webView.plugin.inAppWebViewManager != null) { + Message resultMsg = webView.plugin.inAppWebViewManager.windowWebViewMessages.get(windowId); + if (resultMsg != null) { + ((WebView.WebViewTransport) resultMsg.obj).setWebView(webView); + resultMsg.sendToTarget(); + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + // for some reason, if a WebView is created using a window id, + // the initial plugin and user scripts injected + // with WebViewCompat.addDocumentStartJavaScript will not be added! + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1455 + // + // Also, calling the prepareAndAddUserScripts method right after won't work, + // so use the View.post method here. + webView.post(new Runnable() { + @Override + public void run() { + if (webView != null) { + webView.prepareAndAddUserScripts(); + } + } + }); + } + } + } + } else { + if (initialFile != null) { + try { + webView.loadFile(initialFile); + } catch (IOException e) { + Log.e(LOG_TAG, initialFile + " asset file cannot be found!", e); + } + } + else if (initialData != null) { + String data = initialData.get("data"); + String mimeType = initialData.get("mimeType"); + String encoding = initialData.get("encoding"); + String baseUrl = initialData.get("baseUrl"); + String historyUrl = initialData.get("historyUrl"); + webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); + } + else if (initialUrlRequest != null) { + URLRequest urlRequest = URLRequest.fromMap(initialUrlRequest); + if (urlRequest != null) { + webView.loadUrl(urlRequest); + } + } + } + } + + @Override + public void dispose() { + if (keepAliveId == null && webView != null) { + webView.dispose(); + webView = null; + + if (pullToRefreshLayout != null) { + pullToRefreshLayout.dispose(); + pullToRefreshLayout = null; + } + } + } + + @Override + public void onInputConnectionLocked() { + if (webView != null && webView.inAppBrowserDelegate == null && !webView.customSettings.useHybridComposition) + webView.lockInputConnection(); + } + + @Override + public void onInputConnectionUnlocked() { + if (webView != null && webView.inAppBrowserDelegate == null && !webView.customSettings.useHybridComposition) + webView.unlockInputConnection(); + } + + @Override + public void onFlutterViewAttached(@NonNull View flutterView) { + if (webView != null && !webView.customSettings.useHybridComposition) { + webView.setContainerView(flutterView); + } + } + + @Override + public void onFlutterViewDetached() { + if (webView != null && !webView.customSettings.useHybridComposition) { + webView.setContainerView(null); + } + } +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebView.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebView.java new file mode 100644 index 0000000..b0bfd55 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebView.java @@ -0,0 +1,2101 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import static android.content.Context.INPUT_METHOD_SERVICE; +import static com.pichillilorenzo.flutter_inappwebview_android.types.PreferredContentModeOptionType.fromValue; + +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintManager; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.webkit.CookieManager; +import android.webkit.DownloadListener; +import android.webkit.URLUtil; +import android.webkit.ValueCallback; +import android.webkit.WebBackForwardList; +import android.webkit.WebChromeClient; +import android.webkit.WebHistoryItem; +import android.webkit.WebSettings; +import android.webkit.WebStorage; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebSettingsCompat; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.R; +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.content_blocker.ContentBlocker; +import com.pichillilorenzo.flutter_inappwebview_android.content_blocker.ContentBlockerAction; +import com.pichillilorenzo.flutter_inappwebview_android.content_blocker.ContentBlockerHandler; +import com.pichillilorenzo.flutter_inappwebview_android.content_blocker.ContentBlockerTrigger; +import com.pichillilorenzo.flutter_inappwebview_android.find_interaction.FindInteractionController; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.InterceptAjaxRequestJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.InterceptFetchRequestJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.OnLoadResourceJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.OnWindowBlurEventJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.OnWindowFocusEventJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.PluginScriptsUtil; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.PrintJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.PromisePolyfillJS; +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobController; +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobSettings; +import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshLayout; +import com.pichillilorenzo.flutter_inappwebview_android.types.ContentWorld; +import com.pichillilorenzo.flutter_inappwebview_android.types.DownloadStartRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.PreferredContentModeOptionType; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserContentController; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebViewAssetLoaderExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.ContextMenuSettings; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface; +import com.pichillilorenzo.flutter_inappwebview_android.webview.JavaScriptBridgeInterface; +import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageChannel; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageListener; + +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Pattern; + +import io.flutter.plugin.common.MethodChannel; + +final public class InAppWebView extends InputAwareWebView implements InAppWebViewInterface { + protected static final String LOG_TAG = "InAppWebView"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_"; + + @Nullable + public InAppWebViewFlutterPlugin plugin; + @Nullable + public InAppBrowserDelegate inAppBrowserDelegate; + public Object id; + @Nullable + public Integer windowId; + @Nullable + public InAppWebViewClient inAppWebViewClient; + @Nullable + public InAppWebViewClientCompat inAppWebViewClientCompat; + @Nullable + public InAppWebViewChromeClient inAppWebViewChromeClient; + @Nullable + public InAppWebViewRenderProcessClient inAppWebViewRenderProcessClient; + @Nullable + public WebViewChannelDelegate channelDelegate; + @Nullable + public JavaScriptBridgeInterface javaScriptBridgeInterface; + public InAppWebViewSettings customSettings = new InAppWebViewSettings(); + public boolean isLoading = false; + private boolean inFullscreen = false; + public float zoomScale = 1.0f; + public ContentBlockerHandler contentBlockerHandler = new ContentBlockerHandler(); + public Pattern regexToCancelSubFramesLoadingCompiled; + @Nullable + public GestureDetector gestureDetector = null; + @Nullable + public LinearLayout floatingContextMenu = null; + @Nullable + public Map contextMenu = null; + public Handler mainLooperHandler = new Handler(getWebViewLooper()); + static Handler mHandler = new Handler(); + + public Runnable checkScrollStoppedTask; + public int initialPositionScrollStoppedTask; + public int newCheckScrollStoppedTask = 100; // ms + + public Runnable checkContextMenuShouldBeClosedTask; + public int newCheckContextMenuShouldBeClosedTaskTask = 100; // ms + + public UserContentController userContentController = new UserContentController(this); + + public Map> callAsyncJavaScriptCallbacks = new HashMap<>(); + public Map> evaluateJavaScriptContentWorldCallbacks = new HashMap<>(); + + public Map webMessageChannels = new HashMap<>(); + public List webMessageListeners = new ArrayList<>(); + + private List initialUserOnlyScripts = new ArrayList<>(); + + @Nullable + public FindInteractionController findInteractionController; + + @Nullable + public WebViewAssetLoaderExt webViewAssetLoaderExt; + + @Nullable + private PluginScript interceptOnlyAsyncAjaxRequestsPluginScript; + + public InAppWebView(Context context) { + super(context); + } + + public InAppWebView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public InAppWebView(Context context, AttributeSet attrs, int defaultStyle) { + super(context, attrs, defaultStyle); + } + + public InAppWebView(Context context, @NonNull InAppWebViewFlutterPlugin plugin, + @NonNull Object id, @Nullable Integer windowId, InAppWebViewSettings customSettings, + @Nullable Map contextMenu, View containerView, + List userScripts) { + super(context, containerView, customSettings.useHybridComposition); + this.plugin = plugin; + this.id = id; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id); + this.channelDelegate = new WebViewChannelDelegate(this, channel); + this.windowId = windowId; + this.customSettings = customSettings; + this.contextMenu = contextMenu; + this.initialUserOnlyScripts = userScripts; + if (plugin != null && plugin.activity != null) { + plugin.activity.registerForContextMenu(this); + } + } + + public WebViewClient createWebViewClient(InAppBrowserDelegate inAppBrowserDelegate) { + // bug https://bugs.chromium.org/p/chromium/issues/detail?id=925887 + PackageInfo packageInfo = WebViewCompat.getCurrentWebViewPackage(getContext()); + if (packageInfo == null) { + Log.d(LOG_TAG, "Using InAppWebViewClient implementation"); + return new InAppWebViewClient(inAppBrowserDelegate); + } + + boolean isChromiumWebView = "com.android.webview".equals(packageInfo.packageName) || + "com.google.android.webview".equals(packageInfo.packageName) || + "com.android.chrome".equals(packageInfo.packageName); + boolean isChromiumWebViewBugFixed = false; + if (isChromiumWebView) { + String versionName = packageInfo.versionName != null ? packageInfo.versionName : ""; + try { + int majorVersion = versionName.contains(".") ? + Integer.parseInt(versionName.split("\\.")[0]) : 0; + isChromiumWebViewBugFixed = majorVersion >= 73; + } catch (NumberFormatException ignored) {} + } + + if (isChromiumWebViewBugFixed || !isChromiumWebView) { + Log.d(LOG_TAG, "Using InAppWebViewClientCompat implementation"); + return new InAppWebViewClientCompat(inAppBrowserDelegate); + } else { + Log.d(LOG_TAG, "Using InAppWebViewClient implementation"); + return new InAppWebViewClient(inAppBrowserDelegate); + } + } + + @SuppressLint("RestrictedApi") + public void prepare() { + if (plugin != null) { + webViewAssetLoaderExt = WebViewAssetLoaderExt.fromMap(customSettings.webViewAssetLoader, plugin, getContext()); + } + + javaScriptBridgeInterface = new JavaScriptBridgeInterface(this); + addJavascriptInterface(javaScriptBridgeInterface, JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME); + + inAppWebViewChromeClient = new InAppWebViewChromeClient(plugin, this, inAppBrowserDelegate); + setWebChromeClient(inAppWebViewChromeClient); + + WebViewClient webViewClient = createWebViewClient(inAppBrowserDelegate); + if (webViewClient instanceof InAppWebViewClientCompat) { + inAppWebViewClientCompat = (InAppWebViewClientCompat) webViewClient; + setWebViewClient(inAppWebViewClientCompat); + } else if (webViewClient instanceof InAppWebViewClient) { + inAppWebViewClient = (InAppWebViewClient) webViewClient; + setWebViewClient(inAppWebViewClient); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE)) { + inAppWebViewRenderProcessClient = new InAppWebViewRenderProcessClient(); + WebViewCompat.setWebViewRenderProcessClient(this, inAppWebViewRenderProcessClient); + } + + if (windowId == null || !WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + // for some reason, if a WebView is created using a window id, + // the initial plugin and user scripts injected + // with WebViewCompat.addDocumentStartJavaScript will not be added! + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1455 + prepareAndAddUserScripts(); + } + + if (customSettings.useOnDownloadStart) + setDownloadListener(new DownloadStartListener()); + + WebSettings settings = getSettings(); + + settings.setJavaScriptEnabled(customSettings.javaScriptEnabled); + settings.setJavaScriptCanOpenWindowsAutomatically(customSettings.javaScriptCanOpenWindowsAutomatically); + settings.setBuiltInZoomControls(customSettings.builtInZoomControls); + settings.setDisplayZoomControls(customSettings.displayZoomControls); + settings.setSupportMultipleWindows(customSettings.supportMultipleWindows); + + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) + WebSettingsCompat.setSafeBrowsingEnabled(settings, customSettings.safeBrowsingEnabled); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + settings.setSafeBrowsingEnabled(customSettings.safeBrowsingEnabled); + + settings.setMediaPlaybackRequiresUserGesture(customSettings.mediaPlaybackRequiresUserGesture); + + settings.setDatabaseEnabled(customSettings.databaseEnabled); + settings.setDomStorageEnabled(customSettings.domStorageEnabled); + + if (customSettings.userAgent != null && !customSettings.userAgent.isEmpty()) + settings.setUserAgentString(customSettings.userAgent); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + settings.setUserAgentString(WebSettings.getDefaultUserAgent(getContext())); + + if (customSettings.applicationNameForUserAgent != null && !customSettings.applicationNameForUserAgent.isEmpty()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + String userAgent = (customSettings.userAgent != null && !customSettings.userAgent.isEmpty()) ? customSettings.userAgent : WebSettings.getDefaultUserAgent(getContext()); + String userAgentWithApplicationName = userAgent + " " + customSettings.applicationNameForUserAgent; + settings.setUserAgentString(userAgentWithApplicationName); + } + } + + if (customSettings.clearCache) + clearAllCache(); + else if (customSettings.clearSessionCache) + CookieManager.getInstance().removeSessionCookie(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + CookieManager.getInstance().setAcceptThirdPartyCookies(this, customSettings.thirdPartyCookiesEnabled); + + settings.setLoadWithOverviewMode(customSettings.loadWithOverviewMode); + settings.setUseWideViewPort(customSettings.useWideViewPort); + settings.setSupportZoom(customSettings.supportZoom); + settings.setTextZoom(customSettings.textZoom); + + setVerticalScrollBarEnabled(!customSettings.disableVerticalScroll && customSettings.verticalScrollBarEnabled); + setHorizontalScrollBarEnabled(!customSettings.disableHorizontalScroll && customSettings.horizontalScrollBarEnabled); + + if (customSettings.transparentBackground) + setBackgroundColor(Color.TRANSPARENT); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && customSettings.mixedContentMode != null) + settings.setMixedContentMode(customSettings.mixedContentMode); + + settings.setAllowContentAccess(customSettings.allowContentAccess); + settings.setAllowFileAccess(customSettings.allowFileAccess); + settings.setAllowFileAccessFromFileURLs(customSettings.allowFileAccessFromFileURLs); + settings.setAllowUniversalAccessFromFileURLs(customSettings.allowUniversalAccessFromFileURLs); + setCacheEnabled(customSettings.cacheEnabled); + if (customSettings.appCachePath != null && !customSettings.appCachePath.isEmpty() && customSettings.cacheEnabled) { + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCachePath(customSettings.appCachePath); + Util.invokeMethodIfExists(settings, "setAppCachePath", customSettings.appCachePath); + } + settings.setBlockNetworkImage(customSettings.blockNetworkImage); + settings.setBlockNetworkLoads(customSettings.blockNetworkLoads); + if (customSettings.cacheMode != null) + settings.setCacheMode(customSettings.cacheMode); + settings.setCursiveFontFamily(customSettings.cursiveFontFamily); + settings.setDefaultFixedFontSize(customSettings.defaultFixedFontSize); + settings.setDefaultFontSize(customSettings.defaultFontSize); + settings.setDefaultTextEncodingName(customSettings.defaultTextEncodingName); + if (customSettings.disabledActionModeMenuItems != null) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS)) + WebSettingsCompat.setDisabledActionModeMenuItems(settings, customSettings.disabledActionModeMenuItems); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + settings.setDisabledActionModeMenuItems(customSettings.disabledActionModeMenuItems); + } + settings.setFantasyFontFamily(customSettings.fantasyFontFamily); + settings.setFixedFontFamily(customSettings.fixedFontFamily); + if (customSettings.forceDark != null) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) + WebSettingsCompat.setForceDark(settings, customSettings.forceDark); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + settings.setForceDark(customSettings.forceDark); + } + if (customSettings.forceDarkStrategy != null && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + WebSettingsCompat.setForceDarkStrategy(settings, customSettings.forceDarkStrategy); + } + settings.setGeolocationEnabled(customSettings.geolocationEnabled); + if (customSettings.layoutAlgorithm != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && customSettings.layoutAlgorithm.equals(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING)) { + settings.setLayoutAlgorithm(customSettings.layoutAlgorithm); + } else { + settings.setLayoutAlgorithm(customSettings.layoutAlgorithm); + } + } + settings.setLoadsImagesAutomatically(customSettings.loadsImagesAutomatically); + settings.setMinimumFontSize(customSettings.minimumFontSize); + settings.setMinimumLogicalFontSize(customSettings.minimumLogicalFontSize); + setInitialScale(customSettings.initialScale); + settings.setNeedInitialFocus(customSettings.needInitialFocus); + if (WebViewFeature.isFeatureSupported(WebViewFeature.OFF_SCREEN_PRERASTER)) + WebSettingsCompat.setOffscreenPreRaster(settings, customSettings.offscreenPreRaster); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + settings.setOffscreenPreRaster(customSettings.offscreenPreRaster); + settings.setSansSerifFontFamily(customSettings.sansSerifFontFamily); + settings.setSerifFontFamily(customSettings.serifFontFamily); + settings.setStandardFontFamily(customSettings.standardFontFamily); + if (customSettings.preferredContentMode != null && + customSettings.preferredContentMode == PreferredContentModeOptionType.DESKTOP.toValue()) { + setDesktopMode(true); + } + settings.setSaveFormData(customSettings.saveFormData); + if (customSettings.incognito) + setIncognito(true); + if (customSettings.useHybridComposition) { + if (customSettings.hardwareAcceleration) + setLayerType(View.LAYER_TYPE_HARDWARE, null); + else + setLayerType(View.LAYER_TYPE_NONE, null); + } + if (customSettings.regexToCancelSubFramesLoading != null) { + regexToCancelSubFramesLoadingCompiled = Pattern.compile(customSettings.regexToCancelSubFramesLoading); + } + setScrollBarStyle(customSettings.scrollBarStyle); + if (customSettings.scrollBarDefaultDelayBeforeFade != null) { + setScrollBarDefaultDelayBeforeFade(customSettings.scrollBarDefaultDelayBeforeFade); + } else { + customSettings.scrollBarDefaultDelayBeforeFade = getScrollBarDefaultDelayBeforeFade(); + } + setScrollbarFadingEnabled(customSettings.scrollbarFadingEnabled); + if (customSettings.scrollBarFadeDuration != null) { + setScrollBarFadeDuration(customSettings.scrollBarFadeDuration); + } else { + customSettings.scrollBarFadeDuration = getScrollBarFadeDuration(); + } + setVerticalScrollbarPosition(customSettings.verticalScrollbarPosition); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (customSettings.verticalScrollbarThumbColor != null) + setVerticalScrollbarThumbDrawable(new ColorDrawable(Color.parseColor(customSettings.verticalScrollbarThumbColor))); + if (customSettings.verticalScrollbarTrackColor != null) + setVerticalScrollbarTrackDrawable(new ColorDrawable(Color.parseColor(customSettings.verticalScrollbarTrackColor))); + if (customSettings.horizontalScrollbarThumbColor != null) + setHorizontalScrollbarThumbDrawable(new ColorDrawable(Color.parseColor(customSettings.horizontalScrollbarThumbColor))); + if (customSettings.horizontalScrollbarTrackColor != null) + setHorizontalScrollbarTrackDrawable(new ColorDrawable(Color.parseColor(customSettings.horizontalScrollbarTrackColor))); + } + + setOverScrollMode(customSettings.overScrollMode); + if (customSettings.networkAvailable != null) { + setNetworkAvailable(customSettings.networkAvailable); + } + if (customSettings.rendererPriorityPolicy != null && !customSettings.rendererPriorityPolicy.isEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setRendererPriorityPolicy( + (int) customSettings.rendererPriorityPolicy.get("rendererRequestedPriority"), + (boolean) customSettings.rendererPriorityPolicy.get("waivedWhenNotVisible")); + } + + if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, customSettings.algorithmicDarkeningAllowed); + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY)) { + WebSettingsCompat.setEnterpriseAuthenticationAppLinkPolicyEnabled(settings, customSettings.enterpriseAuthenticationAppLinkPolicyEnabled); + } + if (customSettings.requestedWithHeaderOriginAllowList != null && + WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) { + WebSettingsCompat.setRequestedWithHeaderOriginAllowList(settings, customSettings.requestedWithHeaderOriginAllowList); + } + + contentBlockerHandler.getRuleList().clear(); + for (Map> contentBlocker : customSettings.contentBlockers) { + // compile ContentBlockerTrigger urlFilter + ContentBlockerTrigger trigger = ContentBlockerTrigger.fromMap(contentBlocker.get("trigger")); + ContentBlockerAction action = ContentBlockerAction.fromMap(contentBlocker.get("action")); + contentBlockerHandler.getRuleList().add(new ContentBlocker(trigger, action)); + } + + setFindListener(new FindListener() { + @Override + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { + if (findInteractionController != null && findInteractionController.channelDelegate != null) + findInteractionController.channelDelegate.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); + if (channelDelegate != null) + channelDelegate.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + }); + + gestureDetector = new GestureDetector(this.getContext(), new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent ev) { + if (floatingContextMenu != null) { + hideContextMenu(); + } + return super.onSingleTapUp(ev); + } + }); + + checkScrollStoppedTask = new Runnable() { + @Override + public void run() { + int newPosition = getScrollY(); + if (initialPositionScrollStoppedTask - newPosition == 0) { + // has stopped + onScrollStopped(); + } else { + initialPositionScrollStoppedTask = getScrollY(); + mainLooperHandler.postDelayed(checkScrollStoppedTask, newCheckScrollStoppedTask); + } + } + }; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !customSettings.useHybridComposition) { + checkContextMenuShouldBeClosedTask = new Runnable() { + @Override + public void run() { + if (floatingContextMenu != null) { + evaluateJavascript(PluginScriptsUtil.CHECK_CONTEXT_MENU_SHOULD_BE_HIDDEN_JS_SOURCE, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (value == null || value.equals("true")) { + if (floatingContextMenu != null) { + hideContextMenu(); + } + } else { + mainLooperHandler.postDelayed(checkContextMenuShouldBeClosedTask, newCheckContextMenuShouldBeClosedTaskTask); + } + } + }); + } + } + }; + } + + setOnTouchListener(new OnTouchListener() { + float m_downX; + float m_downY; + + @Override + public boolean onTouch(View v, MotionEvent event) { + gestureDetector.onTouchEvent(event); + + if (event.getAction() == MotionEvent.ACTION_UP) { + checkScrollStoppedTask.run(); + } + + if (customSettings.disableHorizontalScroll && customSettings.disableVerticalScroll) { + return (event.getAction() == MotionEvent.ACTION_MOVE); + } else if (customSettings.disableHorizontalScroll || customSettings.disableVerticalScroll) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + // save the x + m_downX = event.getX(); + // save the y + m_downY = event.getY(); + break; + } + case MotionEvent.ACTION_MOVE: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + if (customSettings.disableHorizontalScroll) { + // set x so that it doesn't move + event.setLocation(m_downX, event.getY()); + } else { + // set y so that it doesn't move + event.setLocation(event.getX(), m_downY); + } + break; + } + } + } + return false; + } + }); + + setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult hitTestResult = + com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult.fromWebViewHitTestResult(getHitTestResult()); + if (channelDelegate != null) channelDelegate.onLongPressHitTestResult(hitTestResult); + return false; + } + }); + } + + public void prepareAndAddUserScripts() { + userContentController.addPluginScript(PromisePolyfillJS.PROMISE_POLYFILL_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(PrintJS.PRINT_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(OnWindowBlurEventJS.ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(OnWindowFocusEventJS.ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT); + interceptOnlyAsyncAjaxRequestsPluginScript = InterceptAjaxRequestJS.createInterceptOnlyAsyncAjaxRequestsPluginScript(customSettings.interceptOnlyAsyncAjaxRequests); + if (customSettings.useShouldInterceptAjaxRequest) { + userContentController.addPluginScript(interceptOnlyAsyncAjaxRequestsPluginScript); + userContentController.addPluginScript(InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT); + } + if (customSettings.useShouldInterceptFetchRequest) { + userContentController.addPluginScript(InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT); + } + if (customSettings.useOnLoadResource) { + userContentController.addPluginScript(OnLoadResourceJS.ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT); + } + if (!customSettings.useHybridComposition) { + userContentController.addPluginScript(PluginScriptsUtil.CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT); + } + this.userContentController.addUserOnlyScripts(this.initialUserOnlyScripts); + } + + public void setIncognito(boolean enabled) { + WebSettings settings = getSettings(); + if (enabled) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().removeAllCookies(null); + } else { + CookieManager.getInstance().removeAllCookie(); + } + + // Disable caching + settings.setCacheMode(WebSettings.LOAD_NO_CACHE); + + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCacheEnabled(false); + Util.invokeMethodIfExists(settings, "setAppCacheEnabled", false); + + clearHistory(); + clearCache(true); + + // No form data or autofill enabled + clearFormData(); + settings.setSavePassword(false); + settings.setSaveFormData(false); + } else { + settings.setCacheMode(WebSettings.LOAD_DEFAULT); + + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCacheEnabled(true); + Util.invokeMethodIfExists(settings, "setAppCacheEnabled", true); + + settings.setSavePassword(true); + settings.setSaveFormData(true); + } + } + + public void setCacheEnabled(boolean enabled) { + WebSettings settings = getSettings(); + if (enabled) { + Context ctx = getContext(); + if (ctx != null) { + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCachePath(ctx.getCacheDir().getAbsolutePath()); + Util.invokeMethodIfExists(settings, "setAppCachePath", ctx.getCacheDir().getAbsolutePath()); + + settings.setCacheMode(WebSettings.LOAD_DEFAULT); + + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCacheEnabled(true); + Util.invokeMethodIfExists(settings, "setAppCacheEnabled", true); + } + } else { + settings.setCacheMode(WebSettings.LOAD_NO_CACHE); + + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCacheEnabled(false); + Util.invokeMethodIfExists(settings, "setAppCacheEnabled", false); + } + } + + public void loadUrl(URLRequest urlRequest) { + String url = urlRequest.getUrl(); + String method = urlRequest.getMethod(); + if (method != null && method.equals("POST")) { + byte[] postData = urlRequest.getBody(); + postUrl(url, postData); + return; + } + Map headers = urlRequest.getHeaders(); + if (headers != null) { + loadUrl(url, headers); + return; + } + loadUrl(url); + } + + public void loadFile(String assetFilePath) throws IOException { + if (plugin == null) { + return; + } + + loadUrl(Util.getUrlAsset(plugin, assetFilePath)); + } + + public boolean isLoading() { + return isLoading; + } + + /** + * @deprecated + */ + @Deprecated + private void clearCookies() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().removeAllCookies(new ValueCallback() { + @Override + public void onReceiveValue(Boolean aBoolean) { + + } + }); + } else { + CookieManager.getInstance().removeAllCookie(); + } + } + + /** + * @deprecated + */ + @Deprecated + public void clearAllCache() { + clearCache(true); + clearCookies(); + clearFormData(); + WebStorage.getInstance().deleteAllData(); + } + + public void takeScreenshot(final @Nullable Map screenshotConfiguration, final MethodChannel.Result result) { + final float pixelDensity = Util.getPixelDensity(getContext()); + + mainLooperHandler.post(new Runnable() { + @Override + public void run() { + try { + Bitmap screenshotBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(screenshotBitmap); + c.translate(-getScrollX(), -getScrollY()); + draw(c); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.PNG; + int quality = 100; + + if (screenshotConfiguration != null) { + Map rect = (Map) screenshotConfiguration.get("rect"); + if (rect != null) { + int rectX = (int) Math.floor(rect.get("x") * pixelDensity + 0.5); + int rectY = (int) Math.floor(rect.get("y") * pixelDensity + 0.5); + int rectWidth = Math.min(screenshotBitmap.getWidth(), (int) Math.floor(rect.get("width") * pixelDensity + 0.5)); + int rectHeight = Math.min(screenshotBitmap.getHeight(), (int) Math.floor(rect.get("height") * pixelDensity + 0.5)); + screenshotBitmap = Bitmap.createBitmap( + screenshotBitmap, + rectX, + rectY, + rectWidth, + rectHeight); + } + + Double snapshotWidth = (Double) screenshotConfiguration.get("snapshotWidth"); + if (snapshotWidth != null) { + int dstWidth = (int) Math.floor(snapshotWidth * pixelDensity + 0.5); + float ratioBitmap = (float) screenshotBitmap.getWidth() / (float) screenshotBitmap.getHeight(); + int dstHeight = (int) ((float) dstWidth / ratioBitmap); + screenshotBitmap = Bitmap.createScaledBitmap(screenshotBitmap, dstWidth, dstHeight, true); + } + + try { + compressFormat = Bitmap.CompressFormat.valueOf((String) screenshotConfiguration.get("compressFormat")); + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, "", e); + } + + quality = (Integer) screenshotConfiguration.get("quality"); + } + + screenshotBitmap.compress( + compressFormat, + quality, + byteArrayOutputStream); + + try { + byteArrayOutputStream.close(); + } catch (IOException e) { + Log.e(LOG_TAG, "", e); + } + screenshotBitmap.recycle(); + result.success(byteArrayOutputStream.toByteArray()); + + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, "", e); + result.success(null); + } + } + }); + } + + @SuppressLint("RestrictedApi") + public void setSettings(InAppWebViewSettings newCustomSettings, HashMap newSettingsMap) { + + WebSettings settings = getSettings(); + + if (newSettingsMap.get("javaScriptEnabled") != null && customSettings.javaScriptEnabled != newCustomSettings.javaScriptEnabled) + settings.setJavaScriptEnabled(newCustomSettings.javaScriptEnabled); + + if (newSettingsMap.get("useShouldInterceptAjaxRequest") != null && customSettings.useShouldInterceptAjaxRequest != newCustomSettings.useShouldInterceptAjaxRequest) { + enablePluginScriptAtRuntime( + InterceptAjaxRequestJS.FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE, + newCustomSettings.useShouldInterceptAjaxRequest, + InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT + ); + } + + if (newSettingsMap.get("interceptOnlyAsyncAjaxRequests") != null && customSettings.interceptOnlyAsyncAjaxRequests != newCustomSettings.interceptOnlyAsyncAjaxRequests) { + enablePluginScriptAtRuntime( + InterceptAjaxRequestJS.FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE, + newCustomSettings.interceptOnlyAsyncAjaxRequests, + interceptOnlyAsyncAjaxRequestsPluginScript + ); + } + + if (newSettingsMap.get("useShouldInterceptFetchRequest") != null && customSettings.useShouldInterceptFetchRequest != newCustomSettings.useShouldInterceptFetchRequest) { + enablePluginScriptAtRuntime( + InterceptFetchRequestJS.FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE, + newCustomSettings.useShouldInterceptFetchRequest, + InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT + ); + } + + if (newSettingsMap.get("useOnLoadResource") != null && customSettings.useOnLoadResource != newCustomSettings.useOnLoadResource) { + enablePluginScriptAtRuntime( + OnLoadResourceJS.FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE, + newCustomSettings.useOnLoadResource, + OnLoadResourceJS.ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT + ); + } + + if (newSettingsMap.get("javaScriptCanOpenWindowsAutomatically") != null && customSettings.javaScriptCanOpenWindowsAutomatically != newCustomSettings.javaScriptCanOpenWindowsAutomatically) + settings.setJavaScriptCanOpenWindowsAutomatically(newCustomSettings.javaScriptCanOpenWindowsAutomatically); + + if (newSettingsMap.get("builtInZoomControls") != null && customSettings.builtInZoomControls != newCustomSettings.builtInZoomControls) + settings.setBuiltInZoomControls(newCustomSettings.builtInZoomControls); + + if (newSettingsMap.get("displayZoomControls") != null && customSettings.displayZoomControls != newCustomSettings.displayZoomControls) + settings.setDisplayZoomControls(newCustomSettings.displayZoomControls); + + if (newSettingsMap.get("safeBrowsingEnabled") != null && customSettings.safeBrowsingEnabled != newCustomSettings.safeBrowsingEnabled) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) + WebSettingsCompat.setSafeBrowsingEnabled(settings, newCustomSettings.safeBrowsingEnabled); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + settings.setSafeBrowsingEnabled(newCustomSettings.safeBrowsingEnabled); + } + + if (newSettingsMap.get("mediaPlaybackRequiresUserGesture") != null && customSettings.mediaPlaybackRequiresUserGesture != newCustomSettings.mediaPlaybackRequiresUserGesture) + settings.setMediaPlaybackRequiresUserGesture(newCustomSettings.mediaPlaybackRequiresUserGesture); + + if (newSettingsMap.get("databaseEnabled") != null && customSettings.databaseEnabled != newCustomSettings.databaseEnabled) + settings.setDatabaseEnabled(newCustomSettings.databaseEnabled); + + if (newSettingsMap.get("domStorageEnabled") != null && customSettings.domStorageEnabled != newCustomSettings.domStorageEnabled) + settings.setDomStorageEnabled(newCustomSettings.domStorageEnabled); + + if (newSettingsMap.get("userAgent") != null && !customSettings.userAgent.equals(newCustomSettings.userAgent) && !newCustomSettings.userAgent.isEmpty()) + settings.setUserAgentString(newCustomSettings.userAgent); + + if (newSettingsMap.get("applicationNameForUserAgent") != null && !customSettings.applicationNameForUserAgent.equals(newCustomSettings.applicationNameForUserAgent) && !newCustomSettings.applicationNameForUserAgent.isEmpty()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + String userAgent = (newCustomSettings.userAgent != null && !newCustomSettings.userAgent.isEmpty()) ? newCustomSettings.userAgent : WebSettings.getDefaultUserAgent(getContext()); + String userAgentWithApplicationName = userAgent + " " + customSettings.applicationNameForUserAgent; + settings.setUserAgentString(userAgentWithApplicationName); + } + } + + if (newSettingsMap.get("clearCache") != null && newCustomSettings.clearCache) + clearAllCache(); + else if (newSettingsMap.get("clearSessionCache") != null && newCustomSettings.clearSessionCache) + CookieManager.getInstance().removeSessionCookie(); + + if (newSettingsMap.get("thirdPartyCookiesEnabled") != null && customSettings.thirdPartyCookiesEnabled != newCustomSettings.thirdPartyCookiesEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + CookieManager.getInstance().setAcceptThirdPartyCookies(this, newCustomSettings.thirdPartyCookiesEnabled); + + if (newSettingsMap.get("useWideViewPort") != null && customSettings.useWideViewPort != newCustomSettings.useWideViewPort) + settings.setUseWideViewPort(newCustomSettings.useWideViewPort); + + if (newSettingsMap.get("supportZoom") != null && customSettings.supportZoom != newCustomSettings.supportZoom) + settings.setSupportZoom(newCustomSettings.supportZoom); + + if (newSettingsMap.get("textZoom") != null && !customSettings.textZoom.equals(newCustomSettings.textZoom)) + settings.setTextZoom(newCustomSettings.textZoom); + + if (newSettingsMap.get("verticalScrollBarEnabled") != null && customSettings.verticalScrollBarEnabled != newCustomSettings.verticalScrollBarEnabled) + setVerticalScrollBarEnabled(newCustomSettings.verticalScrollBarEnabled); + + if (newSettingsMap.get("horizontalScrollBarEnabled") != null && customSettings.horizontalScrollBarEnabled != newCustomSettings.horizontalScrollBarEnabled) + setHorizontalScrollBarEnabled(newCustomSettings.horizontalScrollBarEnabled); + + if (newSettingsMap.get("transparentBackground") != null && customSettings.transparentBackground != newCustomSettings.transparentBackground) { + if (newCustomSettings.transparentBackground) { + setBackgroundColor(Color.TRANSPARENT); + } else { + setBackgroundColor(Color.parseColor("#FFFFFF")); + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + if (newSettingsMap.get("mixedContentMode") != null && (customSettings.mixedContentMode == null || !customSettings.mixedContentMode.equals(newCustomSettings.mixedContentMode))) + settings.setMixedContentMode(newCustomSettings.mixedContentMode); + + if (newSettingsMap.get("supportMultipleWindows") != null && customSettings.supportMultipleWindows != newCustomSettings.supportMultipleWindows) + settings.setSupportMultipleWindows(newCustomSettings.supportMultipleWindows); + + if (newSettingsMap.get("useOnDownloadStart") != null && customSettings.useOnDownloadStart != newCustomSettings.useOnDownloadStart) { + if (newCustomSettings.useOnDownloadStart) { + setDownloadListener(new DownloadStartListener()); + } else { + setDownloadListener(null); + } + } + + if (newSettingsMap.get("allowContentAccess") != null && customSettings.allowContentAccess != newCustomSettings.allowContentAccess) + settings.setAllowContentAccess(newCustomSettings.allowContentAccess); + + if (newSettingsMap.get("allowFileAccess") != null && customSettings.allowFileAccess != newCustomSettings.allowFileAccess) + settings.setAllowFileAccess(newCustomSettings.allowFileAccess); + + if (newSettingsMap.get("allowFileAccessFromFileURLs") != null && customSettings.allowFileAccessFromFileURLs != newCustomSettings.allowFileAccessFromFileURLs) + settings.setAllowFileAccessFromFileURLs(newCustomSettings.allowFileAccessFromFileURLs); + + if (newSettingsMap.get("allowUniversalAccessFromFileURLs") != null && customSettings.allowUniversalAccessFromFileURLs != newCustomSettings.allowUniversalAccessFromFileURLs) + settings.setAllowUniversalAccessFromFileURLs(newCustomSettings.allowUniversalAccessFromFileURLs); + + if (newSettingsMap.get("cacheEnabled") != null && customSettings.cacheEnabled != newCustomSettings.cacheEnabled) + setCacheEnabled(newCustomSettings.cacheEnabled); + + if (newSettingsMap.get("appCachePath") != null && (customSettings.appCachePath == null || !customSettings.appCachePath.equals(newCustomSettings.appCachePath))) { + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCachePath(newCustomSettings.appCachePath); + Util.invokeMethodIfExists(settings, "setAppCachePath", newCustomSettings.appCachePath); + } + + if (newSettingsMap.get("blockNetworkImage") != null && customSettings.blockNetworkImage != newCustomSettings.blockNetworkImage) + settings.setBlockNetworkImage(newCustomSettings.blockNetworkImage); + + if (newSettingsMap.get("blockNetworkLoads") != null && customSettings.blockNetworkLoads != newCustomSettings.blockNetworkLoads) + settings.setBlockNetworkLoads(newCustomSettings.blockNetworkLoads); + + if (newSettingsMap.get("cacheMode") != null && !customSettings.cacheMode.equals(newCustomSettings.cacheMode)) + settings.setCacheMode(newCustomSettings.cacheMode); + + if (newSettingsMap.get("cursiveFontFamily") != null && !customSettings.cursiveFontFamily.equals(newCustomSettings.cursiveFontFamily)) + settings.setCursiveFontFamily(newCustomSettings.cursiveFontFamily); + + if (newSettingsMap.get("defaultFixedFontSize") != null && !customSettings.defaultFixedFontSize.equals(newCustomSettings.defaultFixedFontSize)) + settings.setDefaultFixedFontSize(newCustomSettings.defaultFixedFontSize); + + if (newSettingsMap.get("defaultFontSize") != null && !customSettings.defaultFontSize.equals(newCustomSettings.defaultFontSize)) + settings.setDefaultFontSize(newCustomSettings.defaultFontSize); + + if (newSettingsMap.get("defaultTextEncodingName") != null && !customSettings.defaultTextEncodingName.equals(newCustomSettings.defaultTextEncodingName)) + settings.setDefaultTextEncodingName(newCustomSettings.defaultTextEncodingName); + + if (newSettingsMap.get("disabledActionModeMenuItems") != null && + (customSettings.disabledActionModeMenuItems == null || + !customSettings.disabledActionModeMenuItems.equals(newCustomSettings.disabledActionModeMenuItems))) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS)) + WebSettingsCompat.setDisabledActionModeMenuItems(settings, newCustomSettings.disabledActionModeMenuItems); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + settings.setDisabledActionModeMenuItems(newCustomSettings.disabledActionModeMenuItems); + } + + if (newSettingsMap.get("fantasyFontFamily") != null && !customSettings.fantasyFontFamily.equals(newCustomSettings.fantasyFontFamily)) + settings.setFantasyFontFamily(newCustomSettings.fantasyFontFamily); + + if (newSettingsMap.get("fixedFontFamily") != null && !customSettings.fixedFontFamily.equals(newCustomSettings.fixedFontFamily)) + settings.setFixedFontFamily(newCustomSettings.fixedFontFamily); + + if (newSettingsMap.get("forceDark") != null && !customSettings.forceDark.equals(newCustomSettings.forceDark)) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) + WebSettingsCompat.setForceDark(settings, newCustomSettings.forceDark); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + settings.setForceDark(newCustomSettings.forceDark); + } + + if (newSettingsMap.get("forceDarkStrategy") != null && + !customSettings.forceDarkStrategy.equals(newCustomSettings.forceDarkStrategy) && + WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + WebSettingsCompat.setForceDarkStrategy(settings, newCustomSettings.forceDarkStrategy); + } + + if (newSettingsMap.get("geolocationEnabled") != null && customSettings.geolocationEnabled != newCustomSettings.geolocationEnabled) + settings.setGeolocationEnabled(newCustomSettings.geolocationEnabled); + + if (newSettingsMap.get("layoutAlgorithm") != null && customSettings.layoutAlgorithm != newCustomSettings.layoutAlgorithm) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && newCustomSettings.layoutAlgorithm.equals(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING)) { + settings.setLayoutAlgorithm(newCustomSettings.layoutAlgorithm); + } else { + settings.setLayoutAlgorithm(newCustomSettings.layoutAlgorithm); + } + } + + if (newSettingsMap.get("loadWithOverviewMode") != null && customSettings.loadWithOverviewMode != newCustomSettings.loadWithOverviewMode) + settings.setLoadWithOverviewMode(newCustomSettings.loadWithOverviewMode); + + if (newSettingsMap.get("loadsImagesAutomatically") != null && customSettings.loadsImagesAutomatically != newCustomSettings.loadsImagesAutomatically) + settings.setLoadsImagesAutomatically(newCustomSettings.loadsImagesAutomatically); + + if (newSettingsMap.get("minimumFontSize") != null && !customSettings.minimumFontSize.equals(newCustomSettings.minimumFontSize)) + settings.setMinimumFontSize(newCustomSettings.minimumFontSize); + + if (newSettingsMap.get("minimumLogicalFontSize") != null && !customSettings.minimumLogicalFontSize.equals(newCustomSettings.minimumLogicalFontSize)) + settings.setMinimumLogicalFontSize(newCustomSettings.minimumLogicalFontSize); + + if (newSettingsMap.get("initialScale") != null && !customSettings.initialScale.equals(newCustomSettings.initialScale)) + setInitialScale(newCustomSettings.initialScale); + + if (newSettingsMap.get("needInitialFocus") != null && customSettings.needInitialFocus != newCustomSettings.needInitialFocus) + settings.setNeedInitialFocus(newCustomSettings.needInitialFocus); + + if (newSettingsMap.get("offscreenPreRaster") != null && customSettings.offscreenPreRaster != newCustomSettings.offscreenPreRaster) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.OFF_SCREEN_PRERASTER)) + WebSettingsCompat.setOffscreenPreRaster(settings, newCustomSettings.offscreenPreRaster); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + settings.setOffscreenPreRaster(newCustomSettings.offscreenPreRaster); + } + + if (newSettingsMap.get("sansSerifFontFamily") != null && !customSettings.sansSerifFontFamily.equals(newCustomSettings.sansSerifFontFamily)) + settings.setSansSerifFontFamily(newCustomSettings.sansSerifFontFamily); + + if (newSettingsMap.get("serifFontFamily") != null && !customSettings.serifFontFamily.equals(newCustomSettings.serifFontFamily)) + settings.setSerifFontFamily(newCustomSettings.serifFontFamily); + + if (newSettingsMap.get("standardFontFamily") != null && !customSettings.standardFontFamily.equals(newCustomSettings.standardFontFamily)) + settings.setStandardFontFamily(newCustomSettings.standardFontFamily); + + if (newSettingsMap.get("preferredContentMode") != null && !customSettings.preferredContentMode.equals(newCustomSettings.preferredContentMode)) { + switch (fromValue(newCustomSettings.preferredContentMode)) { + case DESKTOP: + setDesktopMode(true); + break; + case MOBILE: + case RECOMMENDED: + setDesktopMode(false); + break; + } + } + + if (newSettingsMap.get("saveFormData") != null && customSettings.saveFormData != newCustomSettings.saveFormData) + settings.setSaveFormData(newCustomSettings.saveFormData); + + if (newSettingsMap.get("incognito") != null && customSettings.incognito != newCustomSettings.incognito) + setIncognito(newCustomSettings.incognito); + + if (customSettings.useHybridComposition) { + if (newSettingsMap.get("hardwareAcceleration") != null && customSettings.hardwareAcceleration != newCustomSettings.hardwareAcceleration) { + if (newCustomSettings.hardwareAcceleration) + setLayerType(View.LAYER_TYPE_HARDWARE, null); + else + setLayerType(View.LAYER_TYPE_NONE, null); + } + } + + if (newSettingsMap.get("regexToCancelSubFramesLoading") != null && (customSettings.regexToCancelSubFramesLoading == null || + !customSettings.regexToCancelSubFramesLoading.equals(newCustomSettings.regexToCancelSubFramesLoading))) { + if (newCustomSettings.regexToCancelSubFramesLoading == null) + regexToCancelSubFramesLoadingCompiled = null; + else + regexToCancelSubFramesLoadingCompiled = Pattern.compile(customSettings.regexToCancelSubFramesLoading); + } + + if (newCustomSettings.contentBlockers != null) { + contentBlockerHandler.getRuleList().clear(); + for (Map> contentBlocker : newCustomSettings.contentBlockers) { + // compile ContentBlockerTrigger urlFilter + ContentBlockerTrigger trigger = ContentBlockerTrigger.fromMap(contentBlocker.get("trigger")); + ContentBlockerAction action = ContentBlockerAction.fromMap(contentBlocker.get("action")); + contentBlockerHandler.getRuleList().add(new ContentBlocker(trigger, action)); + } + } + + if (newSettingsMap.get("scrollBarStyle") != null && !customSettings.scrollBarStyle.equals(newCustomSettings.scrollBarStyle)) + setScrollBarStyle(newCustomSettings.scrollBarStyle); + + if (newSettingsMap.get("scrollBarDefaultDelayBeforeFade") != null && (customSettings.scrollBarDefaultDelayBeforeFade == null || + !customSettings.scrollBarDefaultDelayBeforeFade.equals(newCustomSettings.scrollBarDefaultDelayBeforeFade))) + setScrollBarDefaultDelayBeforeFade(newCustomSettings.scrollBarDefaultDelayBeforeFade); + + if (newSettingsMap.get("scrollbarFadingEnabled") != null && !customSettings.scrollbarFadingEnabled.equals(newCustomSettings.scrollbarFadingEnabled)) + setScrollbarFadingEnabled(newCustomSettings.scrollbarFadingEnabled); + + if (newSettingsMap.get("scrollBarFadeDuration") != null && (customSettings.scrollBarFadeDuration == null || + !customSettings.scrollBarFadeDuration.equals(newCustomSettings.scrollBarFadeDuration))) + setScrollBarFadeDuration(newCustomSettings.scrollBarFadeDuration); + + if (newSettingsMap.get("verticalScrollbarPosition") != null && !customSettings.verticalScrollbarPosition.equals(newCustomSettings.verticalScrollbarPosition)) + setVerticalScrollbarPosition(newCustomSettings.verticalScrollbarPosition); + + if (newSettingsMap.get("disableVerticalScroll") != null && customSettings.disableVerticalScroll != newCustomSettings.disableVerticalScroll) + setVerticalScrollBarEnabled(!newCustomSettings.disableVerticalScroll && newCustomSettings.verticalScrollBarEnabled); + + if (newSettingsMap.get("disableHorizontalScroll") != null && customSettings.disableHorizontalScroll != newCustomSettings.disableHorizontalScroll) + setHorizontalScrollBarEnabled(!newCustomSettings.disableHorizontalScroll && newCustomSettings.horizontalScrollBarEnabled); + + if (newSettingsMap.get("overScrollMode") != null && !customSettings.overScrollMode.equals(newCustomSettings.overScrollMode)) + setOverScrollMode(newCustomSettings.overScrollMode); + + if (newSettingsMap.get("networkAvailable") != null && customSettings.networkAvailable != newCustomSettings.networkAvailable) + setNetworkAvailable(newCustomSettings.networkAvailable); + + if (newSettingsMap.get("rendererPriorityPolicy") != null && (customSettings.rendererPriorityPolicy == null || + (customSettings.rendererPriorityPolicy.get("rendererRequestedPriority") != newCustomSettings.rendererPriorityPolicy.get("rendererRequestedPriority") || + customSettings.rendererPriorityPolicy.get("waivedWhenNotVisible") != newCustomSettings.rendererPriorityPolicy.get("waivedWhenNotVisible"))) && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setRendererPriorityPolicy( + (int) newCustomSettings.rendererPriorityPolicy.get("rendererRequestedPriority"), + (boolean) newCustomSettings.rendererPriorityPolicy.get("waivedWhenNotVisible")); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (newSettingsMap.get("verticalScrollbarThumbColor") != null && !Util.objEquals(customSettings.verticalScrollbarThumbColor, newCustomSettings.verticalScrollbarThumbColor)) + setVerticalScrollbarThumbDrawable(new ColorDrawable(Color.parseColor(newCustomSettings.verticalScrollbarThumbColor))); + + if (newSettingsMap.get("verticalScrollbarTrackColor") != null && !Util.objEquals(customSettings.verticalScrollbarTrackColor, newCustomSettings.verticalScrollbarTrackColor)) + setVerticalScrollbarTrackDrawable(new ColorDrawable(Color.parseColor(newCustomSettings.verticalScrollbarTrackColor))); + + if (newSettingsMap.get("horizontalScrollbarThumbColor") != null && !Util.objEquals(customSettings.horizontalScrollbarThumbColor, newCustomSettings.horizontalScrollbarThumbColor)) + setHorizontalScrollbarThumbDrawable(new ColorDrawable(Color.parseColor(newCustomSettings.horizontalScrollbarThumbColor))); + + if (newSettingsMap.get("horizontalScrollbarTrackColor") != null && !Util.objEquals(customSettings.horizontalScrollbarTrackColor, newCustomSettings.horizontalScrollbarTrackColor)) + setHorizontalScrollbarTrackDrawable(new ColorDrawable(Color.parseColor(newCustomSettings.horizontalScrollbarTrackColor))); + } + + if (newSettingsMap.get("algorithmicDarkeningAllowed") != null && + !Util.objEquals(customSettings.algorithmicDarkeningAllowed, newCustomSettings.algorithmicDarkeningAllowed) && + WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, newCustomSettings.algorithmicDarkeningAllowed); + } + if (newSettingsMap.get("enterpriseAuthenticationAppLinkPolicyEnabled") != null && + !Util.objEquals(customSettings.enterpriseAuthenticationAppLinkPolicyEnabled, newCustomSettings.enterpriseAuthenticationAppLinkPolicyEnabled) && + WebViewFeature.isFeatureSupported(WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY)) { + WebSettingsCompat.setEnterpriseAuthenticationAppLinkPolicyEnabled(settings, newCustomSettings.enterpriseAuthenticationAppLinkPolicyEnabled); + } + if (newSettingsMap.get("requestedWithHeaderOriginAllowList") != null && + !Util.objEquals(customSettings.requestedWithHeaderOriginAllowList, newCustomSettings.requestedWithHeaderOriginAllowList) && + WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) { + WebSettingsCompat.setRequestedWithHeaderOriginAllowList(settings, newCustomSettings.requestedWithHeaderOriginAllowList); + } + + if (plugin != null) { + if (webViewAssetLoaderExt != null) { + webViewAssetLoaderExt.dispose(); + } + webViewAssetLoaderExt = WebViewAssetLoaderExt.fromMap(customSettings.webViewAssetLoader, plugin, getContext()); + } + + customSettings = newCustomSettings; + } + + public Map getCustomSettings() { + return (customSettings != null) ? customSettings.getRealSettings(this) : null; + } + + public void enablePluginScriptAtRuntime(final String flagVariable, + final boolean enable, + final PluginScript pluginScript) { + evaluateJavascript("window." + flagVariable, null, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + boolean alreadyLoaded = value != null && !value.equalsIgnoreCase("null"); + if (alreadyLoaded) { + String enableSource = "window." + flagVariable + " = " + enable + ";"; + evaluateJavascript(enableSource, null, null); + if (!enable) { + userContentController.removePluginScript(pluginScript); + } + } else if (enable) { + evaluateJavascript(pluginScript.getSource(), null, null); + userContentController.addPluginScript(pluginScript); + } + } + }); + } + + public void injectDeferredObject(String source, @Nullable final ContentWorld contentWorld, String jsWrapper, @Nullable final ValueCallback resultCallback) { + final String resultUuid = contentWorld != null && !contentWorld.equals(ContentWorld.PAGE) ? UUID.randomUUID().toString() : null; + String scriptToInject = source; + if (jsWrapper != null) { + org.json.JSONArray jsonEsc = new org.json.JSONArray(); + jsonEsc.put(source); + String jsonRepr = jsonEsc.toString(); + String jsonSourceString = jsonRepr.substring(1, jsonRepr.length() - 1); + scriptToInject = String.format(jsWrapper, jsonSourceString); + } + if (resultUuid != null && resultCallback != null) { + evaluateJavaScriptContentWorldCallbacks.put(resultUuid, resultCallback); + scriptToInject = Util.replaceAll(PluginScriptsUtil.EVALUATE_JAVASCRIPT_WITH_CONTENT_WORLD_WRAPPER_JS_SOURCE, + PluginScriptsUtil.VAR_RANDOM_NAME, "_" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_" + Math.round(Math.random() * 1000000)) + .replace(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, UserContentController.escapeCode(source)) + .replace(PluginScriptsUtil.VAR_RESULT_UUID, resultUuid); + } + final String finalScriptToInject = scriptToInject; + mainLooperHandler.post(new Runnable() { + @Override + public void run() { + String scriptToInject = userContentController.generateCodeForScriptEvaluation(finalScriptToInject, contentWorld); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + // This action will have the side-effect of blurring the currently focused element + loadUrl("javascript:" + scriptToInject.replaceAll("[\r\n]+", "")); + if (contentWorld != null && resultCallback != null) { + resultCallback.onReceiveValue(""); + } + } else { + evaluateJavascript(scriptToInject, new ValueCallback() { + @Override + public void onReceiveValue(String s) { + if (resultUuid != null || resultCallback == null) + return; + resultCallback.onReceiveValue(s); + } + }); + } + } + }); + } + + public void evaluateJavascript(String source, @Nullable ContentWorld contentWorld, @Nullable ValueCallback resultCallback) { + injectDeferredObject(source, contentWorld, null, resultCallback); + } + + public void injectJavascriptFileFromUrl(String urlFile, @Nullable Map scriptHtmlTagAttributes) { + String scriptAttributes = ""; + if (scriptHtmlTagAttributes != null) { + String typeAttr = (String) scriptHtmlTagAttributes.get("type"); + if (typeAttr != null) { + scriptAttributes += " script.type = '" + typeAttr.replaceAll("'", "\\\\'") + "'; "; + } + String idAttr = (String) scriptHtmlTagAttributes.get("id"); + if (idAttr != null) { + String scriptIdEscaped = idAttr.replaceAll("'", "\\\\'"); + scriptAttributes += " script.id = '" + scriptIdEscaped + "'; "; + scriptAttributes += " script.onload = function() {" + + " if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + " != null) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onInjectedScriptLoaded', '" + scriptIdEscaped + "');" + + " }" + + "};"; + scriptAttributes += " script.onerror = function() {" + + " if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + " != null) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onInjectedScriptError', '" + scriptIdEscaped + "');" + + " }" + + "};"; + } + Boolean asyncAttr = (Boolean) scriptHtmlTagAttributes.get("async"); + if (asyncAttr != null && asyncAttr) { + scriptAttributes += " script.async = true; "; + } + Boolean deferAttr = (Boolean) scriptHtmlTagAttributes.get("defer"); + if (deferAttr != null && deferAttr) { + scriptAttributes += " script.defer = true; "; + } + String crossOriginAttr = (String) scriptHtmlTagAttributes.get("crossOrigin"); + if (crossOriginAttr != null) { + scriptAttributes += " script.crossOrigin = '" + crossOriginAttr.replaceAll("'", "\\\\'") + "'; "; + } + String integrityAttr = (String) scriptHtmlTagAttributes.get("integrity"); + if (integrityAttr != null) { + scriptAttributes += " script.integrity = '" + integrityAttr.replaceAll("'", "\\\\'") + "'; "; + } + Boolean noModuleAttr = (Boolean) scriptHtmlTagAttributes.get("noModule"); + if (noModuleAttr != null && noModuleAttr) { + scriptAttributes += " script.noModule = true; "; + } + String nonceAttr = (String) scriptHtmlTagAttributes.get("nonce"); + if (nonceAttr != null) { + scriptAttributes += " script.nonce = '" + nonceAttr.replaceAll("'", "\\\\'") + "'; "; + } + String referrerPolicyAttr = (String) scriptHtmlTagAttributes.get("referrerPolicy"); + if (referrerPolicyAttr != null) { + scriptAttributes += " script.referrerPolicy = '" + referrerPolicyAttr.replaceAll("'", "\\\\'") + "'; "; + } + } + String jsWrapper = "(function(d) { var script = d.createElement('script'); " + scriptAttributes + + " script.src = %s; if (d.body != null) { d.body.appendChild(script); } })(document);"; + injectDeferredObject(urlFile, null, jsWrapper, null); + } + + public void injectCSSCode(String source) { + String jsWrapper = "(function(d) { var style = d.createElement('style'); style.innerHTML = %s;" + + " if (d.head != null) { d.head.appendChild(style); } })(document);"; + injectDeferredObject(source, null, jsWrapper, null); + } + + public void injectCSSFileFromUrl(String urlFile, @Nullable Map cssLinkHtmlTagAttributes) { + String cssLinkAttributes = ""; + String alternateStylesheet = ""; + if (cssLinkHtmlTagAttributes != null) { + String idAttr = (String) cssLinkHtmlTagAttributes.get("id"); + if (idAttr != null) { + cssLinkAttributes += " link.id = '" + idAttr.replaceAll("'", "\\\\'") + "'; "; + } + String mediaAttr = (String) cssLinkHtmlTagAttributes.get("media"); + if (mediaAttr != null) { + cssLinkAttributes += " link.media = '" + mediaAttr.replaceAll("'", "\\\\'") + "'; "; + } + String crossOriginAttr = (String) cssLinkHtmlTagAttributes.get("crossOrigin"); + if (crossOriginAttr != null) { + cssLinkAttributes += " link.crossOrigin = '" + crossOriginAttr.replaceAll("'", "\\\\'") + "'; "; + } + String integrityAttr = (String) cssLinkHtmlTagAttributes.get("integrity"); + if (integrityAttr != null) { + cssLinkAttributes += " link.integrity = '" + integrityAttr.replaceAll("'", "\\\\'") + "'; "; + } + String referrerPolicyAttr = (String) cssLinkHtmlTagAttributes.get("referrerPolicy"); + if (referrerPolicyAttr != null) { + cssLinkAttributes += " link.referrerPolicy = '" + referrerPolicyAttr.replaceAll("'", "\\\\'") + "'; "; + } + Boolean disabledAttr = (Boolean) cssLinkHtmlTagAttributes.get("disabled"); + if (disabledAttr != null && disabledAttr) { + cssLinkAttributes += " link.disabled = true; "; + } + Boolean alternateAttr = (Boolean) cssLinkHtmlTagAttributes.get("alternate"); + if (alternateAttr != null && alternateAttr) { + alternateStylesheet = "alternate "; + } + String titleAttr = (String) cssLinkHtmlTagAttributes.get("title"); + if (titleAttr != null) { + cssLinkAttributes += " link.title = '" + titleAttr.replaceAll("'", "\\\\'") + "'; "; + } + } + String jsWrapper = "(function(d) { var link = d.createElement('link'); link.rel='" + alternateStylesheet + "stylesheet'; link.type='text/css'; " + + cssLinkAttributes + " link.href = %s; if (d.head != null) { d.head.appendChild(link); } })(document);"; + injectDeferredObject(urlFile, null, jsWrapper, null); + } + + public HashMap getCopyBackForwardList() { + WebBackForwardList currentList = copyBackForwardList(); + int currentSize = currentList.getSize(); + int currentIndex = currentList.getCurrentIndex(); + + List> history = new ArrayList>(); + + for (int i = 0; i < currentSize; i++) { + WebHistoryItem historyItem = currentList.getItemAtIndex(i); + HashMap historyItemMap = new HashMap<>(); + + historyItemMap.put("originalUrl", historyItem.getOriginalUrl()); + historyItemMap.put("title", historyItem.getTitle()); + historyItemMap.put("url", historyItem.getUrl()); + + history.add(historyItemMap); + } + + HashMap result = new HashMap<>(); + + result.put("list", history); + result.put("currentIndex", currentIndex); + + return result; + } + + @Override + protected void onScrollChanged(int x, + int y, + int oldX, + int oldY) { + super.onScrollChanged(x, y, oldX, oldY); + + if (floatingContextMenu != null) { + floatingContextMenu.setAlpha(0f); + floatingContextMenu.setVisibility(View.GONE); + } + + if (channelDelegate != null) channelDelegate.onScrollChanged(x, y); + } + + public void scrollTo(Integer x, Integer y, Boolean animated) { + if (animated) { + PropertyValuesHolder pvhX = PropertyValuesHolder.ofInt("scrollX", x); + PropertyValuesHolder pvhY = PropertyValuesHolder.ofInt("scrollY", y); + ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(this, pvhX, pvhY); + anim.setDuration(300).start(); + } else { + scrollTo(x, y); + } + } + + public void scrollBy(Integer x, Integer y, Boolean animated) { + if (animated) { + PropertyValuesHolder pvhX = PropertyValuesHolder.ofInt("scrollX", getScrollX() + x); + PropertyValuesHolder pvhY = PropertyValuesHolder.ofInt("scrollY", getScrollY() + y); + ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(this, pvhX, pvhY); + anim.setDuration(300).start(); + } else { + scrollBy(x, y); + } + } + + class DownloadStartListener implements DownloadListener { + @Override + public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) { + DownloadStartRequest downloadStartRequest = new DownloadStartRequest( + url, + userAgent, + contentDisposition, + mimeType, + contentLength, + URLUtil.guessFileName(url, contentDisposition, mimeType), + null + ); + if (channelDelegate != null) channelDelegate.onDownloadStartRequest(downloadStartRequest); + } + } + + public void setDesktopMode(final boolean enabled) { + final WebSettings webSettings = getSettings(); + + final String newUserAgent; + if (enabled) { + newUserAgent = webSettings.getUserAgentString().replace("Mobile", "eliboM").replace("Android", "diordnA"); + } else { + newUserAgent = webSettings.getUserAgentString().replace("eliboM", "Mobile").replace("diordnA", "Android"); + } + + webSettings.setUserAgentString(newUserAgent); + webSettings.setUseWideViewPort(enabled); + webSettings.setLoadWithOverviewMode(enabled); + webSettings.setSupportZoom(enabled); + webSettings.setBuiltInZoomControls(enabled); + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + @Nullable + public String printCurrentPage(@Nullable PrintJobSettings settings) { + if (plugin != null && plugin.activity != null) { + // Get a PrintManager instance + PrintManager printManager = (PrintManager) plugin.activity.getSystemService(Context.PRINT_SERVICE); + + if (printManager != null) { + PrintAttributes.Builder builder = new PrintAttributes.Builder(); + + String jobName = (getTitle() != null ? getTitle() : getUrl()) + " Document"; + + if (settings != null) { + if (settings.jobName != null && !settings.jobName.isEmpty()) { + jobName = settings.jobName; + } + if (settings.orientation != null) { + int orientation = settings.orientation; + switch (orientation) { + case 0: + // PORTRAIT + builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_PORTRAIT); + break; + case 1: + // LANDSCAPE + builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE); + break; + } + } +// if (settings.margins != null) { +// // for some reason, Android doesn't set the margins +// builder.setMinMargins(settings.margins.toMargins()); +// } + if (settings.mediaSize != null) { + builder.setMediaSize(settings.mediaSize.toMediaSize()); + } + if (settings.colorMode != null) { + builder.setColorMode(settings.colorMode); + } + if (settings.duplexMode != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + builder.setDuplexMode(settings.duplexMode); + } + if (settings.resolution != null) { + builder.setResolution(settings.resolution.toResolution()); + } + } + + // Get a printCurrentPage adapter instance + PrintDocumentAdapter printAdapter; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + printAdapter = createPrintDocumentAdapter(jobName); + } else { + printAdapter = createPrintDocumentAdapter(); + } + + // Create a printCurrentPage job with name and adapter instance + android.print.PrintJob job = printManager.print(jobName, printAdapter, builder.build()); + + if (settings != null && settings.handledByClient && plugin.printJobManager != null) { + String id = UUID.randomUUID().toString(); + PrintJobController printJobController = new PrintJobController(id, job, settings, plugin); + plugin.printJobManager.jobs.put(printJobController.id, printJobController); + return id; + } + } else { + Log.e(LOG_TAG, "No PrintManager available"); + } + } + return null; + } + + @Override + public void onCreateContextMenu(ContextMenu menu) { + super.onCreateContextMenu(menu); + sendOnCreateContextMenuEvent(); + } + + private void sendOnCreateContextMenuEvent() { + com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult hitTestResult = + com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult.fromWebViewHitTestResult(getHitTestResult()); + if (channelDelegate != null) channelDelegate.onCreateContextMenu(hitTestResult); + } + + private Point contextMenuPoint = new Point(0, 0); + private Point lastTouch = new Point(0, 0); + + @Override + public boolean onTouchEvent(MotionEvent ev) { + lastTouch = new Point((int) ev.getX(), (int) ev.getY()); + + ViewParent parent = getParent(); + if (parent instanceof PullToRefreshLayout) { + PullToRefreshLayout pullToRefreshLayout = (PullToRefreshLayout) parent; + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + pullToRefreshLayout.setEnabled(false); + } + } + + return super.onTouchEvent(ev); + } + + @Override + protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { + super.onOverScrolled(scrollX, scrollY, clampedX, clampedY); + + boolean overScrolledHorizontally = canScrollHorizontally() && clampedX; + boolean overScrolledVertically = canScrollVertically() && clampedY; + + ViewParent parent = getParent(); + if (parent instanceof PullToRefreshLayout && overScrolledVertically && scrollY <= 10) { + PullToRefreshLayout pullToRefreshLayout = (PullToRefreshLayout) parent; + // change over scroll mode to OVER_SCROLL_NEVER in order to disable temporarily the glow effect + setOverScrollMode(OVER_SCROLL_NEVER); + pullToRefreshLayout.setEnabled(pullToRefreshLayout.settings.enabled); + // reset over scroll mode + setOverScrollMode(customSettings.overScrollMode); + } + + if (overScrolledHorizontally || overScrolledVertically) { + if (channelDelegate != null) channelDelegate.onOverScrolled(scrollX, scrollY, overScrolledHorizontally, overScrolledVertically); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + return super.dispatchTouchEvent(event); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + InputConnection connection = super.onCreateInputConnection(outAttrs); + if (connection == null && !customSettings.useHybridComposition && containerView != null) { + // workaround to hide the Keyboard when the user click outside + // on something not focusable such as input or a textarea. + containerView + .getHandler() + .postDelayed( + new Runnable() { + @Override + public void run() { + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); + + boolean isAcceptingText = false; + if (imm != null) { + try { + // imm.isAcceptingText() seems to sometimes crash on some devices + isAcceptingText = imm.isAcceptingText(); + } catch (Exception ignored) { + } + } + + if (containerView != null && imm != null && !isAcceptingText) { + imm.hideSoftInputFromWindow( + containerView.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + } + } + }, + 128); + } + return connection; + } + + @Override + public ActionMode startActionMode(ActionMode.Callback callback) { + if (customSettings.useHybridComposition && !customSettings.disableContextMenu && (contextMenu == null || contextMenu.keySet().size() == 0)) { + return super.startActionMode(callback); + } + return rebuildActionMode(super.startActionMode(callback), callback); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public ActionMode startActionMode(ActionMode.Callback callback, int type) { + if (customSettings.useHybridComposition && !customSettings.disableContextMenu && (contextMenu == null || contextMenu.keySet().size() == 0)) { + return super.startActionMode(callback, type); + } + return rebuildActionMode(super.startActionMode(callback, type), callback); + } + + public ActionMode rebuildActionMode( + final ActionMode actionMode, + final ActionMode.Callback callback + ) { + // fix Android 10 clipboard not working properly https://github.com/pichillilorenzo/flutter_inappwebview/issues/678 + if (!customSettings.useHybridComposition && containerView != null) { + onWindowFocusChanged(containerView.isFocused()); + } + + boolean hasBeenRemovedAndRebuilt = false; + if (floatingContextMenu != null) { + hideContextMenu(); + hasBeenRemovedAndRebuilt = true; + } + if (actionMode == null) { + return null; + } + + Menu actionMenu = actionMode.getMenu(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + actionMode.hide(3000); + } + List defaultMenuItems = new ArrayList<>(); + for (int i = 0; i < actionMenu.size(); i++) { + defaultMenuItems.add(actionMenu.getItem(i)); + } + actionMenu.clear(); + actionMode.finish(); + if (customSettings.disableContextMenu) { + return actionMode; + } + + floatingContextMenu = (LinearLayout) LayoutInflater.from(this.getContext()) + .inflate(R.layout.floating_action_mode, this, false); + HorizontalScrollView horizontalScrollView = (HorizontalScrollView) floatingContextMenu.getChildAt(0); + LinearLayout menuItemListLayout = (LinearLayout) horizontalScrollView.getChildAt(0); + + List> customMenuItems = new ArrayList<>(); + ContextMenuSettings contextMenuSettings = new ContextMenuSettings(); + if (contextMenu != null) { + customMenuItems = (List>) contextMenu.get("menuItems"); + Map contextMenuSettingsMap = (Map) contextMenu.get("settings"); + if (contextMenuSettingsMap != null) { + contextMenuSettings.parse(contextMenuSettingsMap); + } + } + customMenuItems = customMenuItems == null ? new ArrayList>() : customMenuItems; + + if (contextMenuSettings.hideDefaultSystemContextMenuItems == null || !contextMenuSettings.hideDefaultSystemContextMenuItems) { + for (final MenuItem menuItem : defaultMenuItems) { + final int itemId = menuItem.getItemId(); + final String itemTitle = menuItem.getTitle().toString(); + + TextView text = (TextView) LayoutInflater.from(this.getContext()) + .inflate(R.layout.floating_action_mode_item, this, false); + text.setText(itemTitle); + text.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + hideContextMenu(); + callback.onActionItemClicked(actionMode, menuItem); + + if (channelDelegate != null) channelDelegate.onContextMenuActionItemClicked(itemId, itemTitle); + } + }); + if (floatingContextMenu != null) { + menuItemListLayout.addView(text); + } + } + } + + for (final Map menuItem : customMenuItems) { + final int itemId = (int) menuItem.get("id"); + final String itemTitle = (String) menuItem.get("title"); + TextView text = (TextView) LayoutInflater.from(this.getContext()) + .inflate(R.layout.floating_action_mode_item, this, false); + text.setText(itemTitle); + text.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + hideContextMenu(); + + if (channelDelegate != null) channelDelegate.onContextMenuActionItemClicked(itemId, itemTitle); + } + }); + if (floatingContextMenu != null) { + menuItemListLayout.addView(text); + } + } + + final int x = (lastTouch != null) ? lastTouch.x : 0; + final int y = (lastTouch != null) ? lastTouch.y : 0; + contextMenuPoint = new Point(x, y); + + if (floatingContextMenu != null) { + floatingContextMenu.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + + @Override + public void onGlobalLayout() { + if (floatingContextMenu != null) { + floatingContextMenu.getViewTreeObserver().removeOnGlobalLayoutListener(this); + if (getSettings().getJavaScriptEnabled()) { + onScrollStopped(); + } else { + onFloatingActionGlobalLayout(x, y); + } + } + } + }); + addView(floatingContextMenu, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, x, y)); + if (hasBeenRemovedAndRebuilt) { + sendOnCreateContextMenuEvent(); + } + if (checkContextMenuShouldBeClosedTask != null) { + checkContextMenuShouldBeClosedTask.run(); + } + } + + return actionMode; + } + + public void onFloatingActionGlobalLayout(int x, int y) { + int maxWidth = getWidth(); + int maxHeight = getHeight(); + int width = floatingContextMenu.getWidth(); + int height = floatingContextMenu.getHeight(); + int curx = x - (width / 2); + if (curx < 0) { + curx = 0; + } else if (curx + width > maxWidth) { + curx = maxWidth - width; + } + // float size = 12 * scale; + float cury = y - (height * 1.5f); + if (cury < 0) { + cury = y + height; + } + + updateViewLayout( + floatingContextMenu, + new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, curx + getScrollX(), ((int) cury) + getScrollY()) + ); + + mainLooperHandler.post(new Runnable() { + @Override + public void run() { + if (floatingContextMenu != null) { + floatingContextMenu.setVisibility(View.VISIBLE); + floatingContextMenu.animate().alpha(1f).setDuration(100).setListener(null); + } + } + }); + } + + public void hideContextMenu() { + removeView(floatingContextMenu); + floatingContextMenu = null; + + if (channelDelegate != null) channelDelegate.onHideContextMenu(); + } + + public void onScrollStopped() { + if (floatingContextMenu != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + adjustFloatingContextMenuPosition(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public void adjustFloatingContextMenuPosition() { + evaluateJavascript("(function(){" + + " var selection = window.getSelection();" + + " var rangeY = null;" + + " if (selection != null && selection.rangeCount > 0) {" + + " var range = selection.getRangeAt(0);" + + " var clientRect = range.getClientRects();" + + " if (clientRect.length > 0) {" + + " rangeY = clientRect[0].y;" + + " } else if (document.activeElement != null && document.activeElement.tagName.toLowerCase() !== 'iframe') {" + + " var boundingClientRect = document.activeElement.getBoundingClientRect();" + + " rangeY = boundingClientRect.y;" + + " }" + + " }" + + " return rangeY;" + + "})();", new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (floatingContextMenu != null) { + if (value != null && !value.equalsIgnoreCase("null")) { + int x = contextMenuPoint.x; + int y = (int) ((Float.parseFloat(value) * Util.getPixelDensity(getContext())) + (floatingContextMenu.getHeight() / 3.5)); + contextMenuPoint.y = y; + onFloatingActionGlobalLayout(x, y); + } else { + floatingContextMenu.setVisibility(View.VISIBLE); + floatingContextMenu.animate().alpha(1f).setDuration(100).setListener(null); + onFloatingActionGlobalLayout(contextMenuPoint.x, contextMenuPoint.y); + } + } + } + }); + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public void getSelectedText(final ValueCallback resultCallback) { + evaluateJavascript(PluginScriptsUtil.GET_SELECTED_TEXT_JS_SOURCE, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + value = (value != null && !value.equalsIgnoreCase("null")) ? value.substring(1, value.length() - 1) : null; + resultCallback.onReceiveValue(value); + } + }); + } + + public Map requestFocusNodeHref() { + Message msg = InAppWebView.mHandler.obtainMessage(); + requestFocusNodeHref(msg); + Bundle bundle = msg.peekData(); + + Map obj = new HashMap<>(); + obj.put("src", bundle.getString("src")); + obj.put("url", bundle.getString("url")); + obj.put("title", bundle.getString("title")); + + return obj; + } + + public Map requestImageRef() { + Message msg = InAppWebView.mHandler.obtainMessage(); + requestImageRef(msg); + Bundle bundle = msg.peekData(); + + Map obj = new HashMap<>(); + obj.put("url", bundle.getString("url")); + + return obj; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void callAsyncJavaScript(String functionBody, Map arguments, @Nullable ContentWorld contentWorld, @Nullable ValueCallback resultCallback) { + String resultUuid = UUID.randomUUID().toString(); + if (resultCallback != null) { + callAsyncJavaScriptCallbacks.put(resultUuid, resultCallback); + } + + JSONObject functionArguments = new JSONObject(arguments); + Iterator keys = functionArguments.keys(); + + List functionArgumentNamesList = new ArrayList<>(); + List functionArgumentValuesList = new ArrayList<>(); + while (keys.hasNext()) { + String key = keys.next(); + functionArgumentNamesList.add(key); + functionArgumentValuesList.add("obj." + key); + } + + String functionArgumentNames = TextUtils.join(", ", functionArgumentNamesList); + String functionArgumentValues = TextUtils.join(", ", functionArgumentValuesList); + String functionArgumentsObj = Util.JSONStringify(arguments); + + String sourceToInject = PluginScriptsUtil.CALL_ASYNC_JAVA_SCRIPT_WRAPPER_JS_SOURCE + .replace(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES, functionArgumentNames) + .replace(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES, functionArgumentValues) + .replace(PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ, functionArgumentsObj) + .replace(PluginScriptsUtil.VAR_FUNCTION_BODY, functionBody) + .replace(PluginScriptsUtil.VAR_RESULT_UUID, resultUuid) + .replace(PluginScriptsUtil.VAR_RESULT_UUID, resultUuid); + + sourceToInject = userContentController.generateCodeForScriptEvaluation(sourceToInject, contentWorld); + evaluateJavascript(sourceToInject, null); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void isSecureContext(final ValueCallback resultCallback) { + evaluateJavascript("window.isSecureContext", new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("null") + || value.equalsIgnoreCase("false")) { + resultCallback.onReceiveValue(false); + return; + } + resultCallback.onReceiveValue(true); + } + }); + } + + public boolean canScrollVertically() { + return computeVerticalScrollRange() > computeVerticalScrollExtent(); + } + + public boolean canScrollHorizontally() { + return computeHorizontalScrollRange() > computeHorizontalScrollExtent(); + } + + public WebMessageChannel createCompatWebMessageChannel() { + String id = UUID.randomUUID().toString(); + WebMessageChannel webMessageChannel = new WebMessageChannel(id, this); + webMessageChannels.put(id, webMessageChannel); + return webMessageChannel; + } + + @Override + public WebMessageChannel createWebMessageChannel(ValueCallback callback) { + WebMessageChannel webMessageChannel = createCompatWebMessageChannel(); + callback.onReceiveValue(webMessageChannel); + return webMessageChannel; + } + + public void addWebMessageListener(@NonNull WebMessageListener webMessageListener) throws Exception { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + WebViewCompat.addWebMessageListener(this, webMessageListener.jsObjectName, webMessageListener.allowedOriginRules, webMessageListener.listener); + webMessageListeners.add(webMessageListener); + } + } + + public void disposeWebMessageChannels() { + for (WebMessageChannel webMessageChannel : webMessageChannels.values()) { + webMessageChannel.dispose(); + } + webMessageChannels.clear(); + } + + public void disposeWebMessageListeners() { + for (WebMessageListener webMessageListener : webMessageListeners) { + webMessageListener.dispose(); + } + webMessageListeners.clear(); + } + + @Override + public Looper getWebViewLooper() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return super.getWebViewLooper(); + } + return Looper.getMainLooper(); + } + + @Override + public boolean isInFullscreen() { + return inFullscreen; + } + + @Override + public void setInFullscreen(boolean inFullscreen) { + this.inFullscreen = inFullscreen; + } + + @Override + public void postWebMessage(com.pichillilorenzo.flutter_inappwebview_android.types.WebMessage message, Uri targetOrigin, ValueCallback callback) throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + if (customSettings.allowBackgroundAudioPlaying) { + if (visibility != View.GONE) { + super.onWindowVisibilityChanged(View.VISIBLE); + } + return; + } + super.onWindowVisibilityChanged(visibility); + } + + public float getZoomScale() { + return zoomScale; + } + + @Override + public void getZoomScale(final ValueCallback callback) { + callback.onReceiveValue(zoomScale); + } + + @Nullable + public Map getContextMenu() { + return contextMenu; + } + + public void setContextMenu(@Nullable Map contextMenu) { + this.contextMenu = contextMenu; + } + + @Nullable + public InAppWebViewFlutterPlugin getPlugin() { + return plugin; + } + + public void setPlugin(@Nullable InAppWebViewFlutterPlugin plugin) { + this.plugin = plugin; + } + + @Nullable + public InAppBrowserDelegate getInAppBrowserDelegate() { + return inAppBrowserDelegate; + } + + public void setInAppBrowserDelegate(@Nullable InAppBrowserDelegate inAppBrowserDelegate) { + this.inAppBrowserDelegate = inAppBrowserDelegate; + } + + public UserContentController getUserContentController() { + return userContentController; + } + + public void setUserContentController(UserContentController userContentController) { + this.userContentController = userContentController; + } + + public Map getWebMessageChannels() { + return webMessageChannels; + } + + public void setWebMessageChannels(Map webMessageChannels) { + this.webMessageChannels = webMessageChannels; + } + + @Override + public void getContentHeight(ValueCallback callback) { + callback.onReceiveValue(getContentHeight()); + } + + public void getContentWidth(final ValueCallback callback) { + evaluateJavascript("document.documentElement.scrollWidth;", new ValueCallback() { + @Override + public void onReceiveValue(@Nullable String value) { + Integer contentWidth = null; + if (value != null && !value.equalsIgnoreCase("null")) { + contentWidth = Integer.parseInt(value); + } + callback.onReceiveValue(contentWidth); + } + }); + } + + @Override + public void getHitTestResult(ValueCallback callback) { + callback.onReceiveValue(com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult.fromWebViewHitTestResult(getHitTestResult())); + } + + @Nullable + @Override + public WebViewChannelDelegate getChannelDelegate() { + return channelDelegate; + } + + @Override + public void setChannelDelegate(@Nullable WebViewChannelDelegate channelDelegate) { + this.channelDelegate = channelDelegate; + } + + @Override + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + super.dispose(); + WebSettings settings = getSettings(); + settings.setJavaScriptEnabled(false); + removeJavascriptInterface(JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE)) { + WebViewCompat.setWebViewRenderProcessClient(this, null); + } + setWebChromeClient(new WebChromeClient()); + setWebViewClient(new WebViewClient() { + public void onPageFinished(WebView view, String url) { + destroy(); + } + }); + interceptOnlyAsyncAjaxRequestsPluginScript = null; + userContentController.dispose(); + if (findInteractionController != null) { + findInteractionController.dispose(); + findInteractionController = null; + } + if (webViewAssetLoaderExt != null) { + webViewAssetLoaderExt.dispose(); + webViewAssetLoaderExt = null; + } + if (windowId != null && plugin != null && plugin.inAppWebViewManager != null) { + plugin.inAppWebViewManager.windowWebViewMessages.remove(windowId); + } + mainLooperHandler.removeCallbacksAndMessages(null); + mHandler.removeCallbacksAndMessages(null); + disposeWebMessageChannels(); + disposeWebMessageListeners(); + removeAllViews(); + if (checkContextMenuShouldBeClosedTask != null) + removeCallbacks(checkContextMenuShouldBeClosedTask); + if (checkScrollStoppedTask != null) + removeCallbacks(checkScrollStoppedTask); + callAsyncJavaScriptCallbacks.clear(); + evaluateJavaScriptContentWorldCallbacks.clear(); + inAppBrowserDelegate = null; + if (inAppWebViewRenderProcessClient != null) { + inAppWebViewRenderProcessClient.dispose(); + inAppWebViewRenderProcessClient = null; + } + if (inAppWebViewChromeClient != null) { + inAppWebViewChromeClient.dispose(); + inAppWebViewChromeClient = null; + } + if (inAppWebViewClientCompat != null) { + inAppWebViewClientCompat.dispose(); + inAppWebViewClientCompat = null; + } + if (inAppWebViewClient != null) { + inAppWebViewClient.dispose(); + inAppWebViewClient = null; + } + if (javaScriptBridgeInterface != null) { + javaScriptBridgeInterface.dispose(); + javaScriptBridgeInterface = null; + } + plugin = null; + loadUrl("about:blank"); + } + + @Override + public void destroy() { + super.destroy(); + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewChromeClient.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewChromeClient.java new file mode 100644 index 0000000..2f328f3 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewChromeClient.java @@ -0,0 +1,1330 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Message; +import android.os.Parcelable; +import android.provider.MediaStore; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.webkit.ConsoleMessage; +import android.webkit.GeolocationPermissions; +import android.webkit.JsPromptResult; +import android.webkit.JsResult; +import android.webkit.MimeTypeMap; +import android.webkit.PermissionRequest; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFileProvider; +import com.pichillilorenzo.flutter_inappwebview_android.types.CreateWindowAction; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.ActivityResultListener; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.GeolocationPermissionShowPromptResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsAlertResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsBeforeUnloadResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsConfirmResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsPromptResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.PermissionResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.PluginRegistry; + +import static android.app.Activity.RESULT_OK; + +public class InAppWebViewChromeClient extends WebChromeClient implements PluginRegistry.ActivityResultListener, ActivityResultListener { + + protected static final String LOG_TAG = "IABWebChromeClient"; + private InAppBrowserDelegate inAppBrowserDelegate; + + private static final int PICKER = 1; + private static final int PICKER_LEGACY = 3; + final String DEFAULT_MIME_TYPES = "*/*"; + final Map dialogs = new HashMap(); + + protected static final FrameLayout.LayoutParams FULLSCREEN_LAYOUT_PARAMS = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER); + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + protected static final int FULLSCREEN_SYSTEM_UI_VISIBILITY_KITKAT = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + protected static final int FULLSCREEN_SYSTEM_UI_VISIBILITY = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN; + + @Nullable + private View mCustomView; + @Nullable + private WebChromeClient.CustomViewCallback mCustomViewCallback; + private int mOriginalOrientation; + private int mOriginalSystemUiVisibility; + @Nullable + public InAppWebViewFlutterPlugin plugin; + @Nullable + public InAppWebView inAppWebView; + + @Nullable + private ValueCallback filePathCallbackLegacy; + @Nullable + private ValueCallback filePathCallback; + @Nullable + private Uri videoOutputFileUri; + @Nullable + private Uri imageOutputFileUri; + + public InAppWebViewChromeClient(@NonNull final InAppWebViewFlutterPlugin plugin, + @NonNull InAppWebView inAppWebView, InAppBrowserDelegate inAppBrowserDelegate) { + super(); + this.plugin = plugin; + this.inAppWebView = inAppWebView; + this.inAppBrowserDelegate = inAppBrowserDelegate; + if (this.inAppBrowserDelegate != null) { + this.inAppBrowserDelegate.getActivityResultListeners().add(this); + } + + if (plugin.activityPluginBinding != null) + plugin.activityPluginBinding.addActivityResultListener(this); + } + + @Nullable + @Override + public Bitmap getDefaultVideoPoster() { + if (inAppWebView != null && inAppWebView.customSettings.defaultVideoPoster != null) { + final byte[] data = inAppWebView.customSettings.defaultVideoPoster; + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inMutable = true; + return BitmapFactory.decodeByteArray( + data, 0, data.length, bitmapOptions + ); + } + return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); + } + + @Override + public void onHideCustomView() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + View decorView = getRootView(); + if (decorView == null) { + return; + } + if (this.mCustomView != null) { + ((FrameLayout) decorView).removeView(this.mCustomView); + } + this.mCustomView = null; + decorView.setSystemUiVisibility(this.mOriginalSystemUiVisibility); + activity.setRequestedOrientation(this.mOriginalOrientation); + if (this.mCustomViewCallback != null) { + this.mCustomViewCallback.onCustomViewHidden(); + } + this.mCustomViewCallback = null; + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + + if (inAppWebView != null) { + WebViewChannelDelegate eventWebViewChannelDelegate = inAppWebView.channelDelegate; + if (eventWebViewChannelDelegate != null) + eventWebViewChannelDelegate.onExitFullscreen(); + inAppWebView.setInFullscreen(false); + } + } + + @Override + public void onShowCustomView(final View paramView, final CustomViewCallback paramCustomViewCallback) { + if (this.mCustomView != null) { + onHideCustomView(); + return; + } + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + View decorView = getRootView(); + if (decorView == null) { + return; + } + this.mCustomView = paramView; + this.mOriginalSystemUiVisibility = decorView.getSystemUiVisibility(); + this.mOriginalOrientation = activity.getRequestedOrientation(); + this.mCustomViewCallback = paramCustomViewCallback; + if (this.mCustomView != null) { + this.mCustomView.setBackgroundColor(Color.BLACK); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + decorView.setSystemUiVisibility(FULLSCREEN_SYSTEM_UI_VISIBILITY_KITKAT); + } else { + decorView.setSystemUiVisibility(FULLSCREEN_SYSTEM_UI_VISIBILITY); + } + activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + ((FrameLayout) decorView).addView(this.mCustomView, FULLSCREEN_LAYOUT_PARAMS); + + if (inAppWebView != null) { + WebViewChannelDelegate eventWebViewChannelDelegate = inAppWebView.channelDelegate; + if (eventWebViewChannelDelegate != null) + eventWebViewChannelDelegate.onEnterFullscreen(); + inAppWebView.setInFullscreen(true); + } + } + + @Override + public boolean onJsAlert(final WebView view, String url, final String message, + final JsResult result) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onJsAlert(url, message, null, new WebViewChannelDelegate.JsAlertCallback() { + @Override + public boolean nonNullSuccess(@NonNull JsAlertResponse response) { + if (response.isHandledByClient()) { + Integer action = response.getAction(); + action = action != null ? action : 1; + switch (action) { + case 0: + result.confirm(); + break; + case 1: + default: + result.cancel(); + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable JsAlertResponse response) { + String responseMessage = null; + String confirmButtonTitle = null; + if (response != null) { + responseMessage = response.getMessage(); + confirmButtonTitle = response.getConfirmButtonTitle(); + } + createAlertDialog(message, result, responseMessage, confirmButtonTitle); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + result.cancel(); + } + }); + + return true; + } + + return false; + } + + public void createAlertDialog(String message, final JsResult result, String responseMessage, String confirmButtonTitle) { + String alertMessage = (responseMessage != null && !responseMessage.isEmpty()) ? responseMessage : message; + + DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.confirm(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, androidx.appcompat.R.style.Theme_AppCompat_Dialog_Alert); + alertDialogBuilder.setMessage(alertMessage); + if (confirmButtonTitle != null && !confirmButtonTitle.isEmpty()) { + alertDialogBuilder.setPositiveButton(confirmButtonTitle, clickListener); + } else { + alertDialogBuilder.setPositiveButton(android.R.string.ok, clickListener); + } + + alertDialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }); + + AlertDialog alertDialog = alertDialogBuilder.create(); + dialogs.put(alertDialog, result); + alertDialog.show(); + } + + @Override + public boolean onJsConfirm(final WebView view, String url, final String message, + final JsResult result) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onJsConfirm(url, message, null, new WebViewChannelDelegate.JsConfirmCallback() { + @Override + public boolean nonNullSuccess(@NonNull JsConfirmResponse response) { + if (response.isHandledByClient()) { + Integer action = response.getAction(); + action = action != null ? action : 1; + switch (action) { + case 0: + result.confirm(); + break; + case 1: + default: + result.cancel(); + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable JsConfirmResponse response) { + String responseMessage = null; + String confirmButtonTitle = null; + String cancelButtonTitle = null; + if (response != null) { + responseMessage = response.getMessage(); + confirmButtonTitle = response.getConfirmButtonTitle(); + cancelButtonTitle = response.getCancelButtonTitle(); + } + createConfirmDialog(message, result, responseMessage, confirmButtonTitle, cancelButtonTitle); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + result.cancel(); + } + }); + + return true; + } + + return false; + } + + public void createConfirmDialog(String message, final JsResult result, String responseMessage, String confirmButtonTitle, String cancelButtonTitle) { + String alertMessage = (responseMessage != null && !responseMessage.isEmpty()) ? responseMessage : message; + DialogInterface.OnClickListener confirmClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.confirm(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + DialogInterface.OnClickListener cancelClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, androidx.appcompat.R.style.Theme_AppCompat_Dialog_Alert); + alertDialogBuilder.setMessage(alertMessage); + if (confirmButtonTitle != null && !confirmButtonTitle.isEmpty()) { + alertDialogBuilder.setPositiveButton(confirmButtonTitle, confirmClickListener); + } else { + alertDialogBuilder.setPositiveButton(android.R.string.ok, confirmClickListener); + } + if (cancelButtonTitle != null && !cancelButtonTitle.isEmpty()) { + alertDialogBuilder.setNegativeButton(cancelButtonTitle, cancelClickListener); + } else { + alertDialogBuilder.setNegativeButton(android.R.string.cancel, cancelClickListener); + } + + alertDialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }); + + AlertDialog alertDialog = alertDialogBuilder.create(); + dialogs.put(alertDialog, result); + alertDialog.show(); + } + + @Override + public boolean onJsPrompt(final WebView view, String url, final String message, + final String defaultValue, final JsPromptResult result) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onJsPrompt(url, message, defaultValue, null, new WebViewChannelDelegate.JsPromptCallback() { + @Override + public boolean nonNullSuccess(@NonNull JsPromptResponse response) { + if (response.isHandledByClient()) { + Integer action = response.getAction(); + action = action != null ? action : 1; + switch (action) { + case 0: + result.confirm(response.getValue()); + break; + case 1: + default: + result.cancel(); + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable JsPromptResponse response) { + String responseMessage = null; + String responseDefaultValue = null; + String value = null; + String confirmButtonTitle = null; + String cancelButtonTitle = null; + if (response != null) { + responseMessage = response.getMessage(); + responseDefaultValue = response.getDefaultValue(); + value = response.getValue(); + confirmButtonTitle = response.getConfirmButtonTitle(); + cancelButtonTitle = response.getCancelButtonTitle(); + } + createPromptDialog(view, message, defaultValue, result, responseMessage, responseDefaultValue, value, cancelButtonTitle, confirmButtonTitle); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + result.cancel(); + } + }); + + return true; + } + + return false; + } + + public void createPromptDialog(WebView view, String message, String defaultValue, final JsPromptResult result, String responseMessage, String responseDefaultValue, String value, String cancelButtonTitle, String confirmButtonTitle) { + FrameLayout layout = new FrameLayout(view.getContext()); + + final EditText input = new EditText(view.getContext()); + input.setMaxLines(1); + input.setText((responseDefaultValue != null && !responseDefaultValue.isEmpty()) ? responseDefaultValue : defaultValue); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT); + input.setLayoutParams(lp); + + layout.setPaddingRelative(45, 15, 45, 0); + layout.addView(input); + + String alertMessage = (responseMessage != null && !responseMessage.isEmpty()) ? responseMessage : message; + + final String finalValue = value; + DialogInterface.OnClickListener confirmClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String text = input.getText().toString(); + result.confirm(finalValue != null ? finalValue : text); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + DialogInterface.OnClickListener cancelClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, androidx.appcompat.R.style.Theme_AppCompat_Dialog_Alert); + alertDialogBuilder.setMessage(alertMessage); + if (confirmButtonTitle != null && !confirmButtonTitle.isEmpty()) { + alertDialogBuilder.setPositiveButton(confirmButtonTitle, confirmClickListener); + } else { + alertDialogBuilder.setPositiveButton(android.R.string.ok, confirmClickListener); + } + if (cancelButtonTitle != null && !cancelButtonTitle.isEmpty()) { + alertDialogBuilder.setNegativeButton(cancelButtonTitle, cancelClickListener); + } else { + alertDialogBuilder.setNegativeButton(android.R.string.cancel, cancelClickListener); + } + + alertDialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }); + + AlertDialog alertDialog = alertDialogBuilder.create(); + alertDialog.setView(layout); + dialogs.put(alertDialog, result); + alertDialog.show(); + } + + @Override + public boolean onJsBeforeUnload(final WebView view, String url, final String message, + final JsResult result) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onJsBeforeUnload(url, message, new WebViewChannelDelegate.JsBeforeUnloadCallback() { + @Override + public boolean nonNullSuccess(@NonNull JsBeforeUnloadResponse response) { + if (response.isHandledByClient()) { + Integer action = response.getAction(); + action = action != null ? action : 1; + switch (action) { + case 0: + result.confirm(); + break; + case 1: + default: + result.cancel(); + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable JsBeforeUnloadResponse response) { + String responseMessage = null; + String confirmButtonTitle = null; + String cancelButtonTitle = null; + if (response != null) { + responseMessage = response.getMessage(); + confirmButtonTitle = response.getConfirmButtonTitle(); + cancelButtonTitle = response.getCancelButtonTitle(); + } + createBeforeUnloadDialog(message, result, responseMessage, confirmButtonTitle, cancelButtonTitle); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + result.cancel(); + } + }); + + return true; + } + + return false; + } + + public void createBeforeUnloadDialog(String message, final JsResult result, String responseMessage, String confirmButtonTitle, String cancelButtonTitle) { + String alertMessage = (responseMessage != null && !responseMessage.isEmpty()) ? responseMessage : message; + DialogInterface.OnClickListener confirmClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.confirm(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + DialogInterface.OnClickListener cancelClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, androidx.appcompat.R.style.Theme_AppCompat_Dialog_Alert); + alertDialogBuilder.setMessage(alertMessage); + if (confirmButtonTitle != null && !confirmButtonTitle.isEmpty()) { + alertDialogBuilder.setPositiveButton(confirmButtonTitle, confirmClickListener); + } else { + alertDialogBuilder.setPositiveButton(android.R.string.ok, confirmClickListener); + } + if (cancelButtonTitle != null && !cancelButtonTitle.isEmpty()) { + alertDialogBuilder.setNegativeButton(cancelButtonTitle, cancelClickListener); + } else { + alertDialogBuilder.setNegativeButton(android.R.string.cancel, cancelClickListener); + } + + alertDialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }); + + AlertDialog alertDialog = alertDialogBuilder.create(); + dialogs.put(alertDialog, result); + alertDialog.show(); + } + + @Override + public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, final Message resultMsg) { + int windowId = 0; + if (plugin != null && plugin.inAppWebViewManager != null) { + plugin.inAppWebViewManager.windowAutoincrementId++; + windowId = plugin.inAppWebViewManager.windowAutoincrementId; + } + + WebView.HitTestResult result = view.getHitTestResult(); + String url = result.getExtra(); + + // Ensure that images with hyperlink return the correct URL, not the image source + if(result.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { + Message href = view.getHandler().obtainMessage(); + view.requestFocusNodeHref(href); + Bundle data = href.getData(); + if (data != null) { + String imageUrl = data.getString("url"); + if(imageUrl != null && !imageUrl.isEmpty()) { + url = imageUrl; + } + } + } + + URLRequest request = new URLRequest(url, "GET", null, null); + CreateWindowAction createWindowAction = new CreateWindowAction( + request, + true, + isUserGesture, + false, + windowId, + isDialog + ); + + if (plugin != null && plugin.inAppWebViewManager != null) { + plugin.inAppWebViewManager.windowWebViewMessages.put(windowId, resultMsg); + } + + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + final int finalWindowId = windowId; + inAppWebView.channelDelegate.onCreateWindow(createWindowAction, new WebViewChannelDelegate.CreateWindowCallback() { + @Override + public boolean nonNullSuccess(@NonNull Boolean handledByClient) { + return !handledByClient; + } + + @Override + public void defaultBehaviour(@Nullable Boolean handledByClient) { + if (plugin != null && plugin.inAppWebViewManager != null) { + plugin.inAppWebViewManager.windowWebViewMessages.remove(finalWindowId); + } + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }); + + return true; + } + + return false; + } + + @Override + public void onCloseWindow(WebView window) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onCloseWindow(); + } + + super.onCloseWindow(window); + } + + @Override + public void onGeolocationPermissionsShowPrompt(final String origin, final GeolocationPermissions.Callback callback) { + final WebViewChannelDelegate.GeolocationPermissionsShowPromptCallback resultCallback = new WebViewChannelDelegate.GeolocationPermissionsShowPromptCallback() { + @Override + public boolean nonNullSuccess(@NonNull GeolocationPermissionShowPromptResponse response) { + callback.invoke(response.getOrigin(), response.isAllow(), response.isRetain()); + return false; + } + + @Override + public void defaultBehaviour(@Nullable GeolocationPermissionShowPromptResponse response) { + callback.invoke(origin, false, false); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onGeolocationPermissionsShowPrompt(origin, resultCallback); + } else { + resultCallback.defaultBehaviour(null); + } + } + + @Override + public void onGeolocationPermissionsHidePrompt() { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onGeolocationPermissionsHidePrompt(); + } + } + + @Override + public boolean onConsoleMessage(ConsoleMessage consoleMessage) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onConsoleMessage( + consoleMessage.message(), + consoleMessage.messageLevel().ordinal()); + } + return super.onConsoleMessage(consoleMessage); + } + + @Override + public void onProgressChanged(WebView view, int progress) { + super.onProgressChanged(view, progress); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didChangeProgress(progress); + } + + + InAppWebView webView = (InAppWebView) view; + + if (webView.inAppWebViewClientCompat != null) { + webView.inAppWebViewClientCompat.loadCustomJavaScriptOnPageStarted(view); + } else if (webView.inAppWebViewClient != null) { + webView.inAppWebViewClient.loadCustomJavaScriptOnPageStarted(view); + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onProgressChanged(progress); + } + } + + @Override + public void onReceivedTitle(WebView view, String title) { + super.onReceivedTitle(view, title); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didChangeTitle(title); + } + + InAppWebView webView = (InAppWebView) view; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onTitleChanged(title); + } + } + + @Override + public void onReceivedIcon(WebView view, Bitmap icon) { + super.onReceivedIcon(view, icon); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + icon.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); + try { + byteArrayOutputStream.close(); + } catch (IOException e) { + Log.e(LOG_TAG, "", e); + } + icon.recycle(); + + InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedIcon(byteArrayOutputStream.toByteArray()); + } + } + + @Override + public void onReceivedTouchIconUrl(WebView view, + String url, + boolean precomposed) { + super.onReceivedTouchIconUrl(view, url, precomposed); + + InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedTouchIconUrl(url, precomposed); + } + } + + @Nullable + protected ViewGroup getRootView() { + Activity activity = getActivity(); + if (activity == null) { + return null; + } + return (ViewGroup) activity.findViewById(android.R.id.content); + } + + protected void openFileChooser(ValueCallback filePathCallback, String acceptType) { + startPickerIntent(filePathCallback, acceptType, null); + } + + protected void openFileChooser(ValueCallback filePathCallback) { + startPickerIntent(filePathCallback, "", null); + } + + protected void openFileChooser(ValueCallback filePathCallback, String acceptType, String capture) { + startPickerIntent(filePathCallback, acceptType, capture); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { + String[] acceptTypes = fileChooserParams.getAcceptTypes(); + boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE; + boolean captureEnabled = fileChooserParams.isCaptureEnabled(); + return startPickerIntent(filePathCallback, acceptTypes, allowMultiple, captureEnabled); + } + + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + if (filePathCallback == null && filePathCallbackLegacy == null) { + return true; + } + + // based off of which button was pressed, we get an activity result and a file + // the camera activity doesn't properly return the filename* (I think?) so we use + // this filename instead + switch (requestCode) { + case PICKER: + Uri[] results = null; + if (resultCode == RESULT_OK) { + results = getSelectedFiles(data, resultCode); + } + + if (filePathCallback != null) { + filePathCallback.onReceiveValue(results); + } + break; + + case PICKER_LEGACY: + Uri result = null; + if (resultCode == RESULT_OK) { + result = data != null ? data.getData() : getCapturedMediaFile(); + } + if (filePathCallbackLegacy != null) { + filePathCallbackLegacy.onReceiveValue(result); + } + break; + } + + filePathCallback = null; + filePathCallbackLegacy = null; + imageOutputFileUri = null; + videoOutputFileUri = null; + + return true; + } + + private Uri[] getSelectedFiles(Intent data, int resultCode) { + // we have one file selected + if (data != null && data.getData() != null) { + if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return WebChromeClient.FileChooserParams.parseResult(resultCode, data); + } else { + return null; + } + } + + // we have multiple files selected + if (data != null && data.getClipData() != null) { + final int numSelectedFiles = data.getClipData().getItemCount(); + Uri[] result = new Uri[numSelectedFiles]; + for (int i = 0; i < numSelectedFiles; i++) { + result[i] = data.getClipData().getItemAt(i).getUri(); + } + return result; + } + + // we have a captured image or video file + Uri mediaUri = getCapturedMediaFile(); + if (mediaUri != null) { + return new Uri[]{mediaUri}; + } + + return null; + } + + private boolean isFileNotEmpty(Uri uri) { + Activity activity = getActivity(); + if (activity == null) { + return false; + } + + long length; + try { + AssetFileDescriptor descriptor = activity.getContentResolver().openAssetFileDescriptor(uri, "r"); + length = descriptor.getLength(); + descriptor.close(); + } catch (IOException e) { + return false; + } + + return length > 0; + } + + private Uri getCapturedMediaFile() { + if (imageOutputFileUri != null && isFileNotEmpty(imageOutputFileUri)) { + return imageOutputFileUri; + } + + if (videoOutputFileUri != null && isFileNotEmpty(videoOutputFileUri)) { + return videoOutputFileUri; + } + + return null; + } + + public void startPickerIntent(ValueCallback filePathCallback, String acceptType, @Nullable String capture) { + filePathCallbackLegacy = filePathCallback; + + boolean images = acceptsImages(acceptType); + boolean video = acceptsVideo(acceptType); + + Intent pickerIntent = null; + + if (capture != null) { + if (!needsCameraPermission()) { + if (images) { + pickerIntent = getPhotoIntent(); + } + else if (video) { + pickerIntent = getVideoIntent(); + } + } + } + if (pickerIntent == null) { + Intent fileChooserIntent = getFileChooserIntent(acceptType); + pickerIntent = Intent.createChooser(fileChooserIntent, ""); + + ArrayList extraIntents = new ArrayList<>(); + if (!needsCameraPermission()) { + if (images) { + extraIntents.add(getPhotoIntent()); + } + if (video) { + extraIntents.add(getVideoIntent()); + } + } + pickerIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{})); + } + + Activity activity = getActivity(); + if (activity != null && pickerIntent.resolveActivity(activity.getPackageManager()) != null) { + activity.startActivityForResult(pickerIntent, PICKER_LEGACY); + } else { + Log.d(LOG_TAG, "there is no Activity to handle this Intent"); + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public boolean startPickerIntent(final ValueCallback callback, final String[] acceptTypes, + final boolean allowMultiple, final boolean captureEnabled) { + filePathCallback = callback; + + boolean images = acceptsImages(acceptTypes); + boolean video = acceptsVideo(acceptTypes); + + Intent pickerIntent = null; + + if (captureEnabled) { + if (!needsCameraPermission()) { + if (images) { + pickerIntent = getPhotoIntent(); + } + else if (video) { + pickerIntent = getVideoIntent(); + } + } + } + if (pickerIntent == null) { + ArrayList extraIntents = new ArrayList<>(); + if (!needsCameraPermission()) { + if (images) { + extraIntents.add(getPhotoIntent()); + } + if (video) { + extraIntents.add(getVideoIntent()); + } + } + + Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple); + + pickerIntent = new Intent(Intent.ACTION_CHOOSER); + pickerIntent.putExtra(Intent.EXTRA_INTENT, fileSelectionIntent); + pickerIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{})); + } + + Activity activity = getActivity(); + if (activity != null && pickerIntent.resolveActivity(activity.getPackageManager()) != null) { + activity.startActivityForResult(pickerIntent, PICKER); + } else { + Log.d(LOG_TAG, "there is no Activity to handle this Intent"); + } + + return true; + } + + protected boolean needsCameraPermission() { + boolean needed = false; + + Activity activity = getActivity(); + if (activity == null) { + return true; + } + PackageManager packageManager = activity.getPackageManager(); + try { + String[] requestedPermissions = packageManager.getPackageInfo(activity.getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions; + if (Arrays.asList(requestedPermissions).contains(Manifest.permission.CAMERA) + && ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + needed = true; + } + } catch (PackageManager.NameNotFoundException e) { + needed = true; + } + + return needed; + } + + private Intent getPhotoIntent() { + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + imageOutputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, imageOutputFileUri); + return intent; + } + + private Intent getVideoIntent() { + Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + videoOutputFileUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, videoOutputFileUri); + return intent; + } + + private Intent getFileChooserIntent(String acceptTypes) { + String _acceptTypes = acceptTypes; + if (acceptTypes.isEmpty()) { + _acceptTypes = DEFAULT_MIME_TYPES; + } + if (acceptTypes.matches("\\.\\w+")) { + _acceptTypes = getMimeTypeFromExtension(acceptTypes.replace(".", "")); + } + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(_acceptTypes); + return intent; + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + private Intent getFileChooserIntent(String[] acceptTypes, boolean allowMultiple) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes)); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); + return intent; + } + + private Boolean acceptsAny(String[] types) { + if (isArrayEmpty(types)) { + return true; + } + + for (String type : types) { + if (type.equals("*/*")) { + return true; + } + } + + return false; + } + + private Boolean acceptsImages(String types) { + String mimeType = types; + if (types.matches("\\.\\w+")) { + mimeType = getMimeTypeFromExtension(types.replace(".", "")); + } + return mimeType.isEmpty() || mimeType.toLowerCase().contains("image"); + } + + private Boolean acceptsImages(String[] types) { + String[] mimeTypes = getAcceptedMimeType(types); + return acceptsAny(types) || arrayContainsString(mimeTypes, "image"); + } + + private Boolean acceptsVideo(String types) { + String mimeType = types; + if (types.matches("\\.\\w+")) { + mimeType = getMimeTypeFromExtension(types.replace(".", "")); + } + return mimeType.isEmpty() || mimeType.toLowerCase().contains("video"); + } + + private Boolean acceptsVideo(String[] types) { + String[] mimeTypes = getAcceptedMimeType(types); + return acceptsAny(types) || arrayContainsString(mimeTypes, "video"); + } + + private Boolean arrayContainsString(String[] array, String pattern) { + for (String content : array) { + if (content != null && content.contains(pattern)) { + return true; + } + } + return false; + } + + private String[] getAcceptedMimeType(String[] types) { + if (isArrayEmpty(types)) { + return new String[]{DEFAULT_MIME_TYPES}; + } + String[] mimeTypes = new String[types.length]; + for (int i = 0; i < types.length; i++) { + String t = types[i]; + // convert file extensions to mime types + if (t.matches("\\.\\w+")) { + String mimeType = getMimeTypeFromExtension(t.replace(".", "")); + mimeTypes[i] = mimeType; + } else { + mimeTypes[i] = t; + } + } + return mimeTypes; + } + + private String getMimeTypeFromExtension(String extension) { + String type = null; + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + return type; + } + + @Nullable + private Uri getOutputUri(String intentType) { + File capturedFile = null; + try { + capturedFile = getCapturedFile(intentType); + } catch (IOException e) { + Log.e(LOG_TAG, "Error occurred while creating the File", e); + } + if (capturedFile == null) { + return null; + } + + // for versions below 6.0 (23) we use the old File creation & permissions model + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return Uri.fromFile(capturedFile); + } + + Activity activity = getActivity(); + if (activity == null) { + return null; + } + // for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions + String fileProviderAuthority = activity.getApplicationContext().getPackageName() + "." + + InAppWebViewFileProvider.fileProviderAuthorityExtension; + try { + return FileProvider.getUriForFile(activity.getApplicationContext(), + fileProviderAuthority, + capturedFile); + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + return null; + } + + @Nullable + private File getCapturedFile(String intentType) throws IOException { + String prefix = ""; + String suffix = ""; + String dir = ""; + + if (intentType.equals(MediaStore.ACTION_IMAGE_CAPTURE)) { + prefix = "image"; + suffix = ".jpg"; + dir = Environment.DIRECTORY_PICTURES; + } else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) { + prefix = "video"; + suffix = ".mp4"; + dir = Environment.DIRECTORY_MOVIES; + } + + // for versions below 6.0 (23) we use the old File creation & permissions model + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // only this Directory works on all tested Android versions + // ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21) + File storageDir = Environment.getExternalStoragePublicDirectory(dir); + String filename = String.format("%s-%d%s", prefix, System.currentTimeMillis(), suffix); + return new File(storageDir, filename); + } + + Activity activity = getActivity(); + if (activity == null) { + return null; + } + File storageDir = activity.getApplicationContext().getExternalFilesDir(null); + return File.createTempFile(prefix, suffix, storageDir); + } + + private Boolean isArrayEmpty(String[] arr) { + // when our array returned from getAcceptTypes() has no values set from the webview + // i.e. , without any "accept" attr + // will be an array with one empty string element, afaik + return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0); + } + + @Override + public void onPermissionRequest(final PermissionRequest request) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + final WebViewChannelDelegate.PermissionRequestCallback callback = new WebViewChannelDelegate.PermissionRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull PermissionResponse response) { + Integer action = response.getAction(); + if (action != null) { + switch (action) { + case 1: + String[] resources = new String[response.getResources().size()]; + resources = response.getResources().toArray(resources); + request.grant(resources); + break; + case 0: + default: + request.deny(); + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable PermissionResponse response) { + request.deny(); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if(inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onPermissionRequest(request.getOrigin().toString(), + Arrays.asList(request.getResources()), null, callback); + } else { + callback.defaultBehaviour(null); + } + } + } + + @Override + public void onRequestFocus(WebView view) { + if(inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onRequestFocus(); + } + } + + @Override + public void onPermissionRequestCanceled(PermissionRequest request) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && + inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onPermissionRequestCanceled(request.getOrigin().toString(), + Arrays.asList(request.getResources())); + } + } + + @Nullable + private Activity getActivity() { + if (inAppBrowserDelegate != null) { + return inAppBrowserDelegate.getActivity(); + } else if (plugin != null) { + return plugin.activity; + } + return null; + } + + public void dispose() { + for (Map.Entry dialog : dialogs.entrySet()) { + dialog.getValue().cancel(); + dialog.getKey().dismiss(); + } + dialogs.clear(); + if (plugin != null && plugin.activityPluginBinding != null) { + plugin.activityPluginBinding.removeActivityResultListener(this); + } + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.getActivityResultListeners().clear(); + inAppBrowserDelegate = null; + } + filePathCallbackLegacy = null; + filePathCallback = null; + videoOutputFileUri = null; + imageOutputFileUri = null; + inAppWebView = null; + plugin = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClient.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClient.java new file mode 100644 index 0000000..c964c03 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClient.java @@ -0,0 +1,813 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.net.Uri; +import android.net.http.SslError; +import android.os.Build; +import android.os.Message; +import android.util.Log; +import android.view.KeyEvent; +import android.webkit.ClientCertRequest; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.HttpAuthHandler; +import android.webkit.RenderProcessGoneDetail; +import android.webkit.SafeBrowsingResponse; +import android.webkit.SslErrorHandler; +import android.webkit.ValueCallback; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebResourceRequestCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.credential_database.CredentialDatabase; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.CustomSchemeResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthenticationChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationAction; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationActionPolicy; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceErrorExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceResponseExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; + +public class InAppWebViewClient extends WebViewClient { + + protected static final String LOG_TAG = "IAWebViewClient"; + private InAppBrowserDelegate inAppBrowserDelegate; + private static int previousAuthRequestFailureCount = 0; + private static List credentialsProposed = null; + + public InAppWebViewClient(InAppBrowserDelegate inAppBrowserDelegate) { + super(); + this.inAppBrowserDelegate = inAppBrowserDelegate; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + InAppWebView webView = (InAppWebView) view; + if (webView.customSettings.useShouldOverrideUrlLoading) { + boolean isRedirect = false; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT)) { + isRedirect = WebResourceRequestCompat.isRedirect(request); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + isRedirect = request.isRedirect(); + } + onShouldOverrideUrlLoading( + webView, + request.getUrl().toString(), + request.getMethod(), + request.getRequestHeaders(), + request.isForMainFrame(), + request.hasGesture(), + isRedirect); + if (webView.regexToCancelSubFramesLoadingCompiled != null) { + if (request.isForMainFrame()) + return true; + else { + Matcher m = webView.regexToCancelSubFramesLoadingCompiled.matcher(request.getUrl().toString()); + return m.matches(); + } + } else { + // There isn't any way to load an URL for a frame that is not the main frame, + // so if the request is not for the main frame, the navigation is allowed. + return request.isForMainFrame(); + } + } + return false; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView webView, String url) { + InAppWebView inAppWebView = (InAppWebView) webView; + if (inAppWebView.customSettings.useShouldOverrideUrlLoading) { + onShouldOverrideUrlLoading(inAppWebView, url, "GET", null,true, false, false); + return true; + } + return false; + } + + private void allowShouldOverrideUrlLoading(WebView webView, String url, + @Nullable Map headers, + boolean isForMainFrame) { + if (isForMainFrame) { + // There isn't any way to load an URL for a frame that is not the main frame, + // so call this only on main frame. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + webView.loadUrl(url, headers); + else + webView.loadUrl(url); + } + } + public void onShouldOverrideUrlLoading(final InAppWebView webView, final String url, + final String method, + @Nullable final Map headers, + final boolean isForMainFrame, boolean hasGesture, + boolean isRedirect) { + URLRequest request = new URLRequest(url, method, null, headers); + NavigationAction navigationAction = new NavigationAction( + request, + isForMainFrame, + hasGesture, + isRedirect + ); + + final WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback callback = new WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback() { + @Override + public boolean nonNullSuccess(@NonNull NavigationActionPolicy result) { + switch (result) { + case ALLOW: + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); + break; + case CANCEL: + default: + break; + } + return false; + } + + @Override + public void defaultBehaviour(@Nullable NavigationActionPolicy result) { + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.shouldOverrideUrlLoading(navigationAction, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @SuppressLint("RestrictedApi") + public void loadCustomJavaScriptOnPageStarted(WebView view) { + InAppWebView webView = (InAppWebView) view; + + if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + String source = webView.userContentController.generateWrappedCodeForDocumentStart(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(source, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + source.replaceAll("[\r\n]+", "")); + } + } + } + + public void loadCustomJavaScriptOnPageFinished(WebView view) { + InAppWebView webView = (InAppWebView) view; + + String source = webView.userContentController.generateWrappedCodeForDocumentEnd(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(source, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + source.replaceAll("[\r\n]+", "")); + } + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + final InAppWebView webView = (InAppWebView) view; + webView.isLoading = true; + webView.disposeWebMessageChannels(); + webView.userContentController.resetContentWorlds(); + loadCustomJavaScriptOnPageStarted(webView); + + super.onPageStarted(view, url, favicon); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didStartNavigation(url); + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onLoadStart(url); + } + } + + public void onPageFinished(WebView view, String url) { + final InAppWebView webView = (InAppWebView) view; + webView.isLoading = false; + loadCustomJavaScriptOnPageFinished(webView); + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + super.onPageFinished(view, url); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFinishNavigation(url); + } + + // WebView not storing cookies reliable to local device storage + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().flush(); + } else { + CookieSyncManager.getInstance().sync(); + } + + String js = JavaScriptBridgeJS.PLATFORM_READY_JS_SOURCE; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(js, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + js.replaceAll("[\r\n]+", "")); + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onLoadStop(url); + } + } + + @Override + public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + super.doUpdateVisitedHistory(view, url, isReload); + + // url argument sometimes doesn't contain the new changed URL, so we get it again from the webview. + url = view.getUrl(); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didUpdateVisitedHistory(url); + } + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onUpdateVisitedHistory(url, isReload); + } + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onReceivedError(WebView view, @NonNull WebResourceRequest request, @NonNull WebResourceError error) { + final InAppWebView webView = (InAppWebView) view; + + if (request.isForMainFrame()) { + if (webView.customSettings.disableDefaultErrorPage) { + webView.stopLoading(); + webView.loadUrl("about:blank"); + } + + webView.isLoading = false; + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFailNavigation(request.getUrl().toString(), error.getErrorCode(), error.getDescription().toString()); + } + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedError( + WebResourceRequestExt.fromWebResourceRequest(request), + WebResourceErrorExt.fromWebResourceError(error)); + } + } + + @SuppressLint("RestrictedApi") + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.customSettings.disableDefaultErrorPage) { + webView.stopLoading(); + webView.loadUrl("about:blank"); + } + + webView.isLoading = false; + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFailNavigation(failingUrl, errorCode, description); + } + + WebResourceRequestExt request = new WebResourceRequestExt( + failingUrl, + null, + false, + false, + true, + "GET"); + + WebResourceErrorExt error = new WebResourceErrorExt( + errorCode, + description + ); + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedError( + request, + error); + } + + super.onReceivedError(view, errorCode, description, failingUrl); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { + super.onReceivedHttpError(view, request, errorResponse); + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedHttpError( + WebResourceRequestExt.fromWebResourceRequest(request), + WebResourceResponseExt.fromWebResourceResponse(errorResponse)); + } + } + + @Override + public void onReceivedHttpAuthRequest(final WebView view, final HttpAuthHandler handler, final String host, final String realm) { + final String url = view.getUrl(); + String protocol = "https"; + int port = 0; + + if (url != null) { + try { + URI uri = new URI(url); + protocol = uri.getScheme(); + port = uri.getPort(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + } + + previousAuthRequestFailureCount++; + + if (credentialsProposed == null) + credentialsProposed = CredentialDatabase.getInstance(view.getContext()).getHttpAuthCredentials(host, protocol, realm, port); + + URLCredential credentialProposed = null; + if (credentialsProposed != null && credentialsProposed.size() > 0) { + credentialProposed = credentialsProposed.get(0); + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, realm, port, view.getCertificate(), null); + HttpAuthenticationChallenge challenge = new HttpAuthenticationChallenge(protectionSpace, previousAuthRequestFailureCount, credentialProposed); + + final InAppWebView webView = (InAppWebView) view; + final String finalProtocol = protocol; + final int finalPort = port; + final WebViewChannelDelegate.ReceivedHttpAuthRequestCallback callback = new WebViewChannelDelegate.ReceivedHttpAuthRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull HttpAuthResponse response) { + Integer action = response.getAction(); + if (action != null) { + switch (action) { + case 1: + String username = response.getUsername(); + String password = response.getPassword(); + boolean permanentPersistence = response.isPermanentPersistence(); + if (permanentPersistence) { + CredentialDatabase.getInstance(view.getContext()) + .setHttpAuthCredential(host, finalProtocol, realm, finalPort, username, password); + } + handler.proceed(username, password); + break; + case 2: + if (credentialsProposed.size() > 0) { + URLCredential credential = credentialsProposed.remove(0); + handler.proceed(credential.getUsername(), credential.getPassword()); + } else { + handler.cancel(); + } + // used custom CredentialDatabase! + // handler.useHttpAuthUsernamePassword(); + break; + case 0: + default: + credentialsProposed = null; + previousAuthRequestFailureCount = 0; + handler.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable HttpAuthResponse result) { + InAppWebViewClient.super.onReceivedHttpAuthRequest(view, handler, host, realm); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedHttpAuthRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError sslError) { + final String url = sslError.getUrl(); + String host = ""; + String protocol = "https"; + int port = 0; + + try { + URI uri = new URI(url); + host = uri.getHost(); + protocol = uri.getScheme(); + port = uri.getPort(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, null, port, sslError.getCertificate(), sslError); + ServerTrustChallenge challenge = new ServerTrustChallenge(protectionSpace); + + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback callback = new WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull ServerTrustAuthResponse response) { + Integer action = response.getAction(); + if (action != null) { + switch (action) { + case 1: + handler.proceed(); + break; + case 0: + default: + handler.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable ServerTrustAuthResponse result) { + InAppWebViewClient.super.onReceivedSslError(view, handler, sslError); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedServerTrustAuthRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public void onReceivedClientCertRequest(final WebView view, final ClientCertRequest request) { + final String url = view.getUrl(); + final String host = request.getHost(); + String protocol = "https"; + final int port = request.getPort(); + + if (url != null) { + try { + URI uri = new URI(url); + protocol = uri.getScheme(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, null, port, view.getCertificate(), null); + ClientCertChallenge challenge = new ClientCertChallenge(protectionSpace, request.getPrincipals(), request.getKeyTypes()); + + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.ReceivedClientCertRequestCallback callback = new WebViewChannelDelegate.ReceivedClientCertRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull ClientCertResponse response) { + Integer action = response.getAction(); + if (action != null && webView.plugin != null) { + switch (action) { + case 1: + { + String certificatePath = (String) response.getCertificatePath(); + String certificatePassword = (String) response.getCertificatePassword(); + String keyStoreType = (String) response.getKeyStoreType(); + Util.PrivateKeyAndCertificates privateKeyAndCertificates = + Util.loadPrivateKeyAndCertificate(webView.plugin, certificatePath, certificatePassword, keyStoreType); + if (privateKeyAndCertificates != null) { + request.proceed(privateKeyAndCertificates.privateKey, privateKeyAndCertificates.certificates); + } else { + request.cancel(); + } + } + break; + case 2: + request.ignore(); + break; + case 0: + default: + request.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable ClientCertResponse result) { + InAppWebViewClient.super.onReceivedClientCertRequest(view, request); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedClientCertRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onScaleChanged(WebView view, float oldScale, float newScale) { + super.onScaleChanged(view, oldScale, newScale); + final InAppWebView webView = (InAppWebView) view; + webView.zoomScale = newScale / Util.getPixelDensity(webView.getContext()); + + if (webView.channelDelegate != null) { + webView.channelDelegate.onZoomScaleChanged(oldScale, newScale); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O_MR1) + @Override + public void onSafeBrowsingHit(final WebView view, final WebResourceRequest request, final int threatType, final SafeBrowsingResponse callback) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.SafeBrowsingHitCallback resultCallback = new WebViewChannelDelegate.SafeBrowsingHitCallback() { + @Override + public boolean nonNullSuccess(@NonNull com.pichillilorenzo.flutter_inappwebview_android.types.SafeBrowsingResponse response) { + Integer action = response.getAction(); + if (action != null) { + boolean report = response.isReport(); + switch (action) { + case 0: + callback.backToSafety(report); + break; + case 1: + callback.proceed(report); + break; + case 2: + default: + callback.showInterstitial(report); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable com.pichillilorenzo.flutter_inappwebview_android.types.SafeBrowsingResponse result) { + InAppWebViewClient.super.onSafeBrowsingHit(view, request, threatType, callback); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onSafeBrowsingHit(request.getUrl().toString(), threatType, resultCallback); + } else { + resultCallback.defaultBehaviour(null); + } + } + + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequestExt request) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.webViewAssetLoaderExt != null && webView.webViewAssetLoaderExt.loader != null) { + try { + final Uri uri = Uri.parse(request.getUrl()); + WebResourceResponse webResourceResponse = webView.webViewAssetLoaderExt.loader.shouldInterceptRequest(uri); + if (webResourceResponse != null) { + return webResourceResponse; + } + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + } + + if (webView.customSettings.useShouldInterceptRequest) { + WebResourceResponseExt response = null; + if (webView.channelDelegate != null) { + try { + response = webView.channelDelegate.shouldInterceptRequest(request); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + } + + if (response != null) { + String contentType = response.getContentType(); + String contentEncoding = response.getContentEncoding(); + byte[] data = response.getData(); + Map responseHeaders = response.getHeaders(); + Integer statusCode = response.getStatusCode(); + String reasonPhrase = response.getReasonPhrase(); + + ByteArrayInputStream inputStream = (data != null) ? new ByteArrayInputStream(data) : null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && statusCode != null && reasonPhrase != null) { + return new WebResourceResponse(contentType, contentEncoding, statusCode, reasonPhrase, responseHeaders, inputStream); + } else { + return new WebResourceResponse(contentType, contentEncoding, inputStream); + } + } + + return null; + } + + final String url = request.getUrl(); + String scheme = url.split(":")[0].toLowerCase(); + try { + scheme = Uri.parse(request.getUrl()).getScheme(); + } catch (Exception ignored) {} + + if (webView.customSettings.resourceCustomSchemes != null && webView.customSettings.resourceCustomSchemes.contains(scheme)) { + CustomSchemeResponse customSchemeResponse = null; + if (webView.channelDelegate != null) { + try { + customSchemeResponse = webView.channelDelegate.onLoadResourceWithCustomScheme(request); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + } + + if (customSchemeResponse != null) { + WebResourceResponse response = null; + try { + response = webView.contentBlockerHandler.checkUrl(webView, request, customSchemeResponse.getContentType()); + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + if (response != null) + return response; + return new WebResourceResponse(customSchemeResponse.getContentType(), + customSchemeResponse.getContentType(), + new ByteArrayInputStream(customSchemeResponse.getData())); + } + } + + WebResourceResponse response = null; + if (webView.contentBlockerHandler.getRuleList().size() > 0) { + try { + response = webView.contentBlockerHandler.checkUrl(webView, request); + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + } + return response; + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, final String url) { + WebResourceRequestExt requestExt = new WebResourceRequestExt( + url, null, false, + false, true, "GET" + ); + return shouldInterceptRequest(view, requestExt); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + WebResourceRequestExt requestExt = WebResourceRequestExt.fromWebResourceRequest(request); + return shouldInterceptRequest(view, requestExt); + } + + @Override + public void onFormResubmission(final WebView view, final Message dontResend, final Message resend) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.FormResubmissionCallback callback = new WebViewChannelDelegate.FormResubmissionCallback() { + @Override + public boolean nonNullSuccess(@NonNull Integer action) { + switch (action) { + case 0: + resend.sendToTarget(); + break; + case 1: + default: + dontResend.sendToTarget(); + } + return false; + } + + @Override + public void defaultBehaviour(@Nullable Integer result) { + InAppWebViewClient.super.onFormResubmission(view, dontResend, resend); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onFormResubmission(webView.getUrl(), callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onPageCommitVisible(WebView view, String url) { + super.onPageCommitVisible(view, url); + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onPageCommitVisible(url); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.customSettings.useOnRenderProcessGone && webView.channelDelegate != null) { + boolean didCrash = detail.didCrash(); + int rendererPriorityAtExit = detail.rendererPriorityAtExit(); + webView.channelDelegate.onRenderProcessGone(didCrash, rendererPriorityAtExit); + return true; + } + + return super.onRenderProcessGone(view, detail); + } + + @Override + public void onReceivedLoginRequest(WebView view, String realm, String account, String args) { + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedLoginRequest(realm, account, args); + } + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) {} + + public void dispose() { + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate = null; + } + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClientCompat.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClientCompat.java new file mode 100644 index 0000000..f00af41 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClientCompat.java @@ -0,0 +1,840 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.net.Uri; +import android.net.http.SslError; +import android.os.Build; +import android.os.Message; +import android.util.Log; +import android.view.KeyEvent; +import android.webkit.ClientCertRequest; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.HttpAuthHandler; +import android.webkit.RenderProcessGoneDetail; +import android.webkit.SslErrorHandler; +import android.webkit.ValueCallback; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.webkit.SafeBrowsingResponseCompat; +import androidx.webkit.WebResourceErrorCompat; +import androidx.webkit.WebResourceRequestCompat; +import androidx.webkit.WebViewClientCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.credential_database.CredentialDatabase; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.CustomSchemeResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthenticationChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationAction; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationActionPolicy; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceErrorExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceResponseExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; + +public class InAppWebViewClientCompat extends WebViewClientCompat { + + protected static final String LOG_TAG = "IAWebViewClientCompat"; + private InAppBrowserDelegate inAppBrowserDelegate; + private static int previousAuthRequestFailureCount = 0; + private static List credentialsProposed = null; + + public InAppWebViewClientCompat(InAppBrowserDelegate inAppBrowserDelegate) { + super(); + this.inAppBrowserDelegate = inAppBrowserDelegate; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull WebResourceRequest request) { + InAppWebView webView = (InAppWebView) view; + if (webView.customSettings.useShouldOverrideUrlLoading) { + boolean isRedirect = false; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT)) { + isRedirect = WebResourceRequestCompat.isRedirect(request); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + isRedirect = request.isRedirect(); + } + onShouldOverrideUrlLoading( + webView, + request.getUrl().toString(), + request.getMethod(), + request.getRequestHeaders(), + request.isForMainFrame(), + request.hasGesture(), + isRedirect); + if (webView.regexToCancelSubFramesLoadingCompiled != null) { + if (request.isForMainFrame()) + return true; + else { + Matcher m = webView.regexToCancelSubFramesLoadingCompiled.matcher(request.getUrl().toString()); + return m.matches(); + } + } else { + // There isn't any way to load an URL for a frame that is not the main frame, + // so if the request is not for the main frame, the navigation is allowed. + return request.isForMainFrame(); + } + } + return false; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView webView, String url) { + InAppWebView inAppWebView = (InAppWebView) webView; + if (inAppWebView.customSettings.useShouldOverrideUrlLoading) { + onShouldOverrideUrlLoading(inAppWebView, url, "GET", null,true, false, false); + return true; + } + return false; + } + + private void allowShouldOverrideUrlLoading(WebView webView, String url, + @Nullable Map headers, + boolean isForMainFrame) { + if (isForMainFrame) { + // There isn't any way to load an URL for a frame that is not the main frame, + // so call this only on main frame. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + webView.loadUrl(url, headers); + else + webView.loadUrl(url); + } + } + public void onShouldOverrideUrlLoading(final InAppWebView webView, final String url, + final String method, + @Nullable final Map headers, + final boolean isForMainFrame, boolean hasGesture, + boolean isRedirect) { + URLRequest request = new URLRequest(url, method, null, headers); + NavigationAction navigationAction = new NavigationAction( + request, + isForMainFrame, + hasGesture, + isRedirect + ); + + final WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback callback = new WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback() { + @Override + public boolean nonNullSuccess(@NonNull NavigationActionPolicy result) { + switch (result) { + case ALLOW: + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); + break; + case CANCEL: + default: + break; + } + return false; + } + + @Override + public void defaultBehaviour(@Nullable NavigationActionPolicy result) { + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.shouldOverrideUrlLoading(navigationAction, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @SuppressLint("RestrictedApi") + public void loadCustomJavaScriptOnPageStarted(WebView view) { + InAppWebView webView = (InAppWebView) view; + + if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + String source = webView.userContentController.generateWrappedCodeForDocumentStart(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(source, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + source.replaceAll("[\r\n]+", "")); + } + } + } + + public void loadCustomJavaScriptOnPageFinished(WebView view) { + InAppWebView webView = (InAppWebView) view; + + String source = webView.userContentController.generateWrappedCodeForDocumentEnd(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(source, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + source.replaceAll("[\r\n]+", "")); + } + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + final InAppWebView webView = (InAppWebView) view; + webView.isLoading = true; + webView.disposeWebMessageChannels(); + webView.userContentController.resetContentWorlds(); + loadCustomJavaScriptOnPageStarted(webView); + + super.onPageStarted(view, url, favicon); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didStartNavigation(url); + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onLoadStart(url); + } + } + + public void onPageFinished(WebView view, String url) { + final InAppWebView webView = (InAppWebView) view; + webView.isLoading = false; + loadCustomJavaScriptOnPageFinished(webView); + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + super.onPageFinished(view, url); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFinishNavigation(url); + } + + // WebView not storing cookies reliable to local device storage + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().flush(); + } else { + CookieSyncManager.getInstance().sync(); + } + + String js = JavaScriptBridgeJS.PLATFORM_READY_JS_SOURCE; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(js, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + js.replaceAll("[\r\n]+", "")); + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onLoadStop(url); + } + } + + @Override + public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + super.doUpdateVisitedHistory(view, url, isReload); + + // url argument sometimes doesn't contain the new changed URL, so we get it again from the webview. + url = view.getUrl(); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didUpdateVisitedHistory(url); + } + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onUpdateVisitedHistory(url, isReload); + } + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onReceivedError(@NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + final InAppWebView webView = (InAppWebView) view; + + if (request.isForMainFrame()) { + if (webView.customSettings.disableDefaultErrorPage) { + webView.stopLoading(); + webView.loadUrl("about:blank"); + } + + webView.isLoading = false; + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + if (inAppBrowserDelegate != null) { + int type = -1; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) { + type = error.getErrorCode(); + } + String description = ""; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION)) { + description = error.getDescription().toString(); + } + inAppBrowserDelegate.didFailNavigation(request.getUrl().toString(), type, description); + } + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedError( + WebResourceRequestExt.fromWebResourceRequest(request), + WebResourceErrorExt.fromWebResourceError(error)); + } + } + + @SuppressLint("RestrictedApi") + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.customSettings.disableDefaultErrorPage) { + webView.stopLoading(); + webView.loadUrl("about:blank"); + } + + webView.isLoading = false; + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFailNavigation(failingUrl, errorCode, description); + } + + WebResourceRequestExt request = new WebResourceRequestExt( + failingUrl, + null, + false, + false, + true, + "GET"); + + WebResourceErrorExt error = new WebResourceErrorExt( + errorCode, + description + ); + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedError( + request, + error); + } + + super.onReceivedError(view, errorCode, description, failingUrl); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onReceivedHttpError(@NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceResponse errorResponse) { + super.onReceivedHttpError(view, request, errorResponse); + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedHttpError( + WebResourceRequestExt.fromWebResourceRequest(request), + WebResourceResponseExt.fromWebResourceResponse(errorResponse)); + } + } + + @Override + public void onReceivedHttpAuthRequest(final WebView view, final HttpAuthHandler handler, final String host, final String realm) { + final String url = view.getUrl(); + String protocol = "https"; + int port = 0; + + if (url != null) { + try { + URI uri = new URI(url); + protocol = uri.getScheme(); + port = uri.getPort(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + } + + previousAuthRequestFailureCount++; + + if (credentialsProposed == null) + credentialsProposed = CredentialDatabase.getInstance(view.getContext()).getHttpAuthCredentials(host, protocol, realm, port); + + URLCredential credentialProposed = null; + if (credentialsProposed != null && credentialsProposed.size() > 0) { + credentialProposed = credentialsProposed.get(0); + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, realm, port, view.getCertificate(), null); + HttpAuthenticationChallenge challenge = new HttpAuthenticationChallenge(protectionSpace, previousAuthRequestFailureCount, credentialProposed); + + final InAppWebView webView = (InAppWebView) view; + final String finalProtocol = protocol; + final int finalPort = port; + final WebViewChannelDelegate.ReceivedHttpAuthRequestCallback callback = new WebViewChannelDelegate.ReceivedHttpAuthRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull HttpAuthResponse response) { + Integer action = response.getAction(); + if (action != null) { + switch (action) { + case 1: + String username = response.getUsername(); + String password = response.getPassword(); + boolean permanentPersistence = response.isPermanentPersistence(); + if (permanentPersistence) { + CredentialDatabase.getInstance(view.getContext()) + .setHttpAuthCredential(host, finalProtocol, realm, finalPort, username, password); + } + handler.proceed(username, password); + break; + case 2: + if (credentialsProposed.size() > 0) { + URLCredential credential = credentialsProposed.remove(0); + handler.proceed(credential.getUsername(), credential.getPassword()); + } else { + handler.cancel(); + } + // used custom CredentialDatabase! + // handler.useHttpAuthUsernamePassword(); + break; + case 0: + default: + credentialsProposed = null; + previousAuthRequestFailureCount = 0; + handler.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable HttpAuthResponse result) { + InAppWebViewClientCompat.super.onReceivedHttpAuthRequest(view, handler, host, realm); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedHttpAuthRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError sslError) { + final String url = sslError.getUrl(); + String host = ""; + String protocol = "https"; + int port = 0; + + try { + URI uri = new URI(url); + host = uri.getHost(); + protocol = uri.getScheme(); + port = uri.getPort(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, null, port, sslError.getCertificate(), sslError); + ServerTrustChallenge challenge = new ServerTrustChallenge(protectionSpace); + + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback callback = new WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull ServerTrustAuthResponse response) { + Integer action = response.getAction(); + if (action != null) { + switch (action) { + case 1: + handler.proceed(); + break; + case 0: + default: + handler.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable ServerTrustAuthResponse result) { + InAppWebViewClientCompat.super.onReceivedSslError(view, handler, sslError); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedServerTrustAuthRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public void onReceivedClientCertRequest(final WebView view, final ClientCertRequest request) { + final String url = view.getUrl(); + final String host = request.getHost(); + String protocol = "https"; + final int port = request.getPort(); + + if (url != null) { + try { + URI uri = new URI(url); + protocol = uri.getScheme(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, null, port, view.getCertificate(), null); + ClientCertChallenge challenge = new ClientCertChallenge(protectionSpace, request.getPrincipals(), request.getKeyTypes()); + + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.ReceivedClientCertRequestCallback callback = new WebViewChannelDelegate.ReceivedClientCertRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull ClientCertResponse response) { + Integer action = response.getAction(); + if (action != null && webView.plugin != null) { + switch (action) { + case 1: + { + String certificatePath = (String) response.getCertificatePath(); + String certificatePassword = (String) response.getCertificatePassword(); + String keyStoreType = (String) response.getKeyStoreType(); + Util.PrivateKeyAndCertificates privateKeyAndCertificates = + Util.loadPrivateKeyAndCertificate(webView.plugin, certificatePath, certificatePassword, keyStoreType); + if (privateKeyAndCertificates != null) { + request.proceed(privateKeyAndCertificates.privateKey, privateKeyAndCertificates.certificates); + } else { + request.cancel(); + } + } + break; + case 2: + request.ignore(); + break; + case 0: + default: + request.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable ClientCertResponse result) { + InAppWebViewClientCompat.super.onReceivedClientCertRequest(view, request); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedClientCertRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onScaleChanged(WebView view, float oldScale, float newScale) { + super.onScaleChanged(view, oldScale, newScale); + final InAppWebView webView = (InAppWebView) view; + webView.zoomScale = newScale / Util.getPixelDensity(webView.getContext()); + + if (webView.channelDelegate != null) { + webView.channelDelegate.onZoomScaleChanged(oldScale, newScale); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O_MR1) + @Override + public void onSafeBrowsingHit(@NonNull final WebView view, + @NonNull final WebResourceRequest request, + final int threatType, + @NonNull final SafeBrowsingResponseCompat callback) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.SafeBrowsingHitCallback resultCallback = new WebViewChannelDelegate.SafeBrowsingHitCallback() { + @Override + public boolean nonNullSuccess(@NonNull com.pichillilorenzo.flutter_inappwebview_android.types.SafeBrowsingResponse response) { + Integer action = response.getAction(); + if (action != null) { + boolean report = response.isReport(); + switch (action) { + case 0: + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY)) { + callback.backToSafety(report); + } else { + return true; + } + break; + case 1: + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED)) { + callback.proceed(report); + } else { + return true; + } + break; + case 2: + default: + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL)) { + callback.showInterstitial(report); + } else { + return true; + } + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable com.pichillilorenzo.flutter_inappwebview_android.types.SafeBrowsingResponse result) { + InAppWebViewClientCompat.super.onSafeBrowsingHit(view, request, threatType, callback); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onSafeBrowsingHit(request.getUrl().toString(), threatType, resultCallback); + } else { + resultCallback.defaultBehaviour(null); + } + } + + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequestExt request) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.webViewAssetLoaderExt != null && webView.webViewAssetLoaderExt.loader != null) { + try { + final Uri uri = Uri.parse(request.getUrl()); + WebResourceResponse webResourceResponse = webView.webViewAssetLoaderExt.loader.shouldInterceptRequest(uri); + if (webResourceResponse != null) { + return webResourceResponse; + } + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + } + + if (webView.customSettings.useShouldInterceptRequest) { + WebResourceResponseExt response = null; + if (webView.channelDelegate != null) { + try { + response = webView.channelDelegate.shouldInterceptRequest(request); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + } + + if (response != null) { + String contentType = response.getContentType(); + String contentEncoding = response.getContentEncoding(); + byte[] data = response.getData(); + Map responseHeaders = response.getHeaders(); + Integer statusCode = response.getStatusCode(); + String reasonPhrase = response.getReasonPhrase(); + + ByteArrayInputStream inputStream = (data != null) ? new ByteArrayInputStream(data) : null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && statusCode != null && reasonPhrase != null) { + return new WebResourceResponse(contentType, contentEncoding, statusCode, reasonPhrase, responseHeaders, inputStream); + } else { + return new WebResourceResponse(contentType, contentEncoding, inputStream); + } + } + + return null; + } + + final String url = request.getUrl(); + String scheme = url.split(":")[0].toLowerCase(); + try { + scheme = Uri.parse(request.getUrl()).getScheme(); + } catch (Exception ignored) {} + + if (webView.customSettings.resourceCustomSchemes != null && webView.customSettings.resourceCustomSchemes.contains(scheme)) { + CustomSchemeResponse customSchemeResponse = null; + if (webView.channelDelegate != null) { + try { + customSchemeResponse = webView.channelDelegate.onLoadResourceWithCustomScheme(request); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + } + + if (customSchemeResponse != null) { + WebResourceResponse response = null; + try { + response = webView.contentBlockerHandler.checkUrl(webView, request, customSchemeResponse.getContentType()); + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + if (response != null) + return response; + return new WebResourceResponse(customSchemeResponse.getContentType(), + customSchemeResponse.getContentType(), + new ByteArrayInputStream(customSchemeResponse.getData())); + } + } + + WebResourceResponse response = null; + if (webView.contentBlockerHandler.getRuleList().size() > 0) { + try { + response = webView.contentBlockerHandler.checkUrl(webView, request); + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + } + return response; + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, final String url) { + WebResourceRequestExt requestExt = new WebResourceRequestExt( + url, null, false, + false, true, "GET" + ); + return shouldInterceptRequest(view, requestExt); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + WebResourceRequestExt requestExt = WebResourceRequestExt.fromWebResourceRequest(request); + return shouldInterceptRequest(view, requestExt); + } + + @Override + public void onFormResubmission(final WebView view, final Message dontResend, final Message resend) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.FormResubmissionCallback callback = new WebViewChannelDelegate.FormResubmissionCallback() { + @Override + public boolean nonNullSuccess(@NonNull Integer action) { + switch (action) { + case 0: + resend.sendToTarget(); + break; + case 1: + default: + dontResend.sendToTarget(); + } + return false; + } + + @Override + public void defaultBehaviour(@Nullable Integer result) { + InAppWebViewClientCompat.super.onFormResubmission(view, dontResend, resend); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onFormResubmission(webView.getUrl(), callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onPageCommitVisible(@NonNull WebView view, @NonNull String url) { + super.onPageCommitVisible(view, url); + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onPageCommitVisible(url); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.customSettings.useOnRenderProcessGone && webView.channelDelegate != null) { + boolean didCrash = detail.didCrash(); + int rendererPriorityAtExit = detail.rendererPriorityAtExit(); + webView.channelDelegate.onRenderProcessGone(didCrash, rendererPriorityAtExit); + return true; + } + + return super.onRenderProcessGone(view, detail); + } + + @Override + public void onReceivedLoginRequest(WebView view, String realm, String account, String args) { + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedLoginRequest(realm, account, args); + } + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) {} + + public void dispose() { + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate = null; + } + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewRenderProcessClient.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewRenderProcessClient.java new file mode 100644 index 0000000..1fe8c50 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewRenderProcessClient.java @@ -0,0 +1,99 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.util.Log; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewFeature; +import androidx.webkit.WebViewRenderProcess; +import androidx.webkit.WebViewRenderProcessClient; + +import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate; + +public class InAppWebViewRenderProcessClient extends WebViewRenderProcessClient { + + protected static final String LOG_TAG = "IAWRenderProcessClient"; + + public InAppWebViewRenderProcessClient() { + super(); + } + + @Override + public void onRenderProcessUnresponsive(@NonNull WebView view, @Nullable final WebViewRenderProcess renderer) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.RenderProcessUnresponsiveCallback callback = new WebViewChannelDelegate.RenderProcessUnresponsiveCallback() { + @Override + public boolean nonNullSuccess(@NonNull Integer action) { + if (renderer != null) { + switch (action) { + case 0: + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_VIEW_RENDERER_TERMINATE)) + renderer.terminate(); + break; + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable Integer result) { + + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onRenderProcessUnresponsive(webView.getUrl(), callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onRenderProcessResponsive(@NonNull WebView view, @Nullable final WebViewRenderProcess renderer) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.RenderProcessResponsiveCallback callback = new WebViewChannelDelegate.RenderProcessResponsiveCallback() { + @Override + public boolean nonNullSuccess(@NonNull Integer action) { + if (renderer != null) { + switch (action) { + case 0: + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_VIEW_RENDERER_TERMINATE)) + renderer.terminate(); + break; + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable Integer result) { + + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onRenderProcessResponsive(webView.getUrl(), callback); + } else { + callback.defaultBehaviour(null); + } + } + + void dispose() { + + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewSettings.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewSettings.java new file mode 100644 index 0000000..91c13ea --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewSettings.java @@ -0,0 +1,647 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import static android.webkit.WebSettings.LayoutAlgorithm.NARROW_COLUMNS; +import static android.webkit.WebSettings.LayoutAlgorithm.NORMAL; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.view.View; +import android.webkit.WebSettings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebSettingsCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; +import com.pichillilorenzo.flutter_inappwebview_android.types.PreferredContentModeOptionType; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class InAppWebViewSettings implements ISettings { + + public static final String LOG_TAG = "InAppWebViewSettings"; + + public Boolean useShouldOverrideUrlLoading = false; + public Boolean useOnLoadResource = false; + public Boolean useOnDownloadStart = false; + /** + * @deprecated + */ + @Deprecated + public Boolean clearCache = false; + public String userAgent = ""; + public String applicationNameForUserAgent = ""; + public Boolean javaScriptEnabled = true; + public Boolean javaScriptCanOpenWindowsAutomatically = false; + public Boolean mediaPlaybackRequiresUserGesture = true; + public Integer minimumFontSize = 8; + public Boolean verticalScrollBarEnabled = true; + public Boolean horizontalScrollBarEnabled = true; + public List resourceCustomSchemes = new ArrayList<>(); + public List>> contentBlockers = new ArrayList<>(); + public Integer preferredContentMode = PreferredContentModeOptionType.RECOMMENDED.toValue(); + public Boolean useShouldInterceptAjaxRequest = false; + public Boolean interceptOnlyAsyncAjaxRequests = true; + public Boolean useShouldInterceptFetchRequest = false; + public Boolean incognito = false; + public Boolean cacheEnabled = true; + public Boolean transparentBackground = false; + public Boolean disableVerticalScroll = false; + public Boolean disableHorizontalScroll = false; + public Boolean disableContextMenu = false; + public Boolean supportZoom = true; + public Boolean allowFileAccessFromFileURLs = false; + public Boolean allowUniversalAccessFromFileURLs = false; + public Boolean allowBackgroundAudioPlaying = false; + public Integer textZoom = 100; + /** + * @deprecated + */ + @Deprecated + public Boolean clearSessionCache = false; + public Boolean builtInZoomControls = true; + public Boolean displayZoomControls = false; + public Boolean databaseEnabled = false; + public Boolean domStorageEnabled = true; + public Boolean useWideViewPort = true; + public Boolean safeBrowsingEnabled = true; + public Integer mixedContentMode; + public Boolean allowContentAccess = true; + public Boolean allowFileAccess = true; + public String appCachePath; + public Boolean blockNetworkImage = false; + public Boolean blockNetworkLoads = false; + public Integer cacheMode = WebSettings.LOAD_DEFAULT; + public String cursiveFontFamily = "cursive"; + public Integer defaultFixedFontSize = 16; + public Integer defaultFontSize = 16; + public String defaultTextEncodingName = "UTF-8"; + public Integer disabledActionModeMenuItems; + public String fantasyFontFamily = "fantasy"; + public String fixedFontFamily = "monospace"; + public Integer forceDark = 0; // WebSettingsCompat.FORCE_DARK_OFF + public Integer forceDarkStrategy = WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING; + public Boolean geolocationEnabled = true; + public WebSettings.LayoutAlgorithm layoutAlgorithm; + public Boolean loadWithOverviewMode = true; + public Boolean loadsImagesAutomatically = true; + public Integer minimumLogicalFontSize = 8; + public Integer initialScale = 0; + public Boolean needInitialFocus = true; + public Boolean offscreenPreRaster = false; + public String sansSerifFontFamily = "sans-serif"; + public String serifFontFamily = "sans-serif"; + public String standardFontFamily = "sans-serif"; + public Boolean saveFormData = true; + public Boolean thirdPartyCookiesEnabled = true; + public Boolean hardwareAcceleration = true; + public Boolean supportMultipleWindows = false; + public String regexToCancelSubFramesLoading; + public Integer overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS; + public Boolean networkAvailable = null; + public Integer scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY; + public Integer verticalScrollbarPosition = View.SCROLLBAR_POSITION_DEFAULT; + public Integer scrollBarDefaultDelayBeforeFade = null; + public Boolean scrollbarFadingEnabled = true; + public Integer scrollBarFadeDuration = null; + @Nullable + public Map rendererPriorityPolicy = null; + public Boolean useShouldInterceptRequest = false; + public Boolean useOnRenderProcessGone = false; + public Boolean disableDefaultErrorPage = false; + public Boolean useHybridComposition = true; + @Nullable + public String verticalScrollbarThumbColor; + @Nullable + public String verticalScrollbarTrackColor; + @Nullable + public String horizontalScrollbarThumbColor; + @Nullable + public String horizontalScrollbarTrackColor; + public Boolean algorithmicDarkeningAllowed = false; + public Boolean enterpriseAuthenticationAppLinkPolicyEnabled = true; + @Nullable + public Map webViewAssetLoader; + @Nullable + public byte[] defaultVideoPoster; + @Nullable + public Set requestedWithHeaderOriginAllowList; + + @NonNull + @Override + public InAppWebViewSettings parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "useShouldOverrideUrlLoading": + useShouldOverrideUrlLoading = (Boolean) value; + break; + case "useOnLoadResource": + useOnLoadResource = (Boolean) value; + break; + case "useOnDownloadStart": + useOnDownloadStart = (Boolean) value; + break; + case "clearCache": + clearCache = (Boolean) value; + break; + case "userAgent": + userAgent = (String) value; + break; + case "applicationNameForUserAgent": + applicationNameForUserAgent = (String) value; + break; + case "javaScriptEnabled": + javaScriptEnabled = (Boolean) value; + break; + case "javaScriptCanOpenWindowsAutomatically": + javaScriptCanOpenWindowsAutomatically = (Boolean) value; + break; + case "mediaPlaybackRequiresUserGesture": + mediaPlaybackRequiresUserGesture = (Boolean) value; + break; + case "minimumFontSize": + minimumFontSize = (Integer) value; + break; + case "verticalScrollBarEnabled": + verticalScrollBarEnabled = (Boolean) value; + break; + case "horizontalScrollBarEnabled": + horizontalScrollBarEnabled = (Boolean) value; + break; + case "resourceCustomSchemes": + resourceCustomSchemes = (List) value; + break; + case "contentBlockers": + contentBlockers = (List>>) value; + break; + case "preferredContentMode": + preferredContentMode = (Integer) value; + break; + case "useShouldInterceptAjaxRequest": + useShouldInterceptAjaxRequest = (Boolean) value; + break; + case "interceptOnlyAsyncAjaxRequests": + interceptOnlyAsyncAjaxRequests = (Boolean) value; + break; + case "useShouldInterceptFetchRequest": + useShouldInterceptFetchRequest = (Boolean) value; + break; + case "incognito": + incognito = (Boolean) value; + break; + case "cacheEnabled": + cacheEnabled = (Boolean) value; + break; + case "transparentBackground": + transparentBackground = (Boolean) value; + break; + case "disableVerticalScroll": + disableVerticalScroll = (Boolean) value; + break; + case "disableHorizontalScroll": + disableHorizontalScroll = (Boolean) value; + break; + case "disableContextMenu": + disableContextMenu = (Boolean) value; + break; + case "textZoom": + textZoom = (Integer) value; + break; + case "clearSessionCache": + clearSessionCache = (Boolean) value; + break; + case "builtInZoomControls": + builtInZoomControls = (Boolean) value; + break; + case "displayZoomControls": + displayZoomControls = (Boolean) value; + break; + case "supportZoom": + supportZoom = (Boolean) value; + break; + case "databaseEnabled": + databaseEnabled = (Boolean) value; + break; + case "domStorageEnabled": + domStorageEnabled = (Boolean) value; + break; + case "useWideViewPort": + useWideViewPort = (Boolean) value; + break; + case "safeBrowsingEnabled": + safeBrowsingEnabled = (Boolean) value; + break; + case "mixedContentMode": + mixedContentMode = (Integer) value; + break; + case "allowContentAccess": + allowContentAccess = (Boolean) value; + break; + case "allowFileAccess": + allowFileAccess = (Boolean) value; + break; + case "allowFileAccessFromFileURLs": + allowFileAccessFromFileURLs = (Boolean) value; + break; + case "allowUniversalAccessFromFileURLs": + allowUniversalAccessFromFileURLs = (Boolean) value; + break; + case "appCachePath": + appCachePath = (String) value; + break; + case "blockNetworkImage": + blockNetworkImage = (Boolean) value; + break; + case "blockNetworkLoads": + blockNetworkLoads = (Boolean) value; + break; + case "cacheMode": + cacheMode = (Integer) value; + break; + case "cursiveFontFamily": + cursiveFontFamily = (String) value; + break; + case "defaultFixedFontSize": + defaultFixedFontSize = (Integer) value; + break; + case "defaultFontSize": + defaultFontSize = (Integer) value; + break; + case "defaultTextEncodingName": + defaultTextEncodingName = (String) value; + break; + case "disabledActionModeMenuItems": + disabledActionModeMenuItems = (Integer) value; + break; + case "fantasyFontFamily": + fantasyFontFamily = (String) value; + break; + case "fixedFontFamily": + fixedFontFamily = (String) value; + break; + case "forceDark": + forceDark = (Integer) value; + break; + case "forceDarkStrategy": + forceDarkStrategy = (Integer) value; + break; + case "geolocationEnabled": + geolocationEnabled = (Boolean) value; + break; + case "layoutAlgorithm": + setLayoutAlgorithm((String) value); + break; + case "loadWithOverviewMode": + loadWithOverviewMode = (Boolean) value; + break; + case "loadsImagesAutomatically": + loadsImagesAutomatically = (Boolean) value; + break; + case "minimumLogicalFontSize": + minimumLogicalFontSize = (Integer) value; + break; + case "initialScale": + initialScale = (Integer) value; + break; + case "needInitialFocus": + needInitialFocus = (Boolean) value; + break; + case "offscreenPreRaster": + offscreenPreRaster = (Boolean) value; + break; + case "sansSerifFontFamily": + sansSerifFontFamily = (String) value; + break; + case "serifFontFamily": + serifFontFamily = (String) value; + break; + case "standardFontFamily": + standardFontFamily = (String) value; + break; + case "saveFormData": + saveFormData = (Boolean) value; + break; + case "thirdPartyCookiesEnabled": + thirdPartyCookiesEnabled = (Boolean) value; + break; + case "hardwareAcceleration": + hardwareAcceleration = (Boolean) value; + break; + case "supportMultipleWindows": + supportMultipleWindows = (Boolean) value; + break; + case "regexToCancelSubFramesLoading": + regexToCancelSubFramesLoading = (String) value; + break; + case "overScrollMode": + overScrollMode = (Integer) value; + break; + case "networkAvailable": + networkAvailable = (Boolean) value; + break; + case "scrollBarStyle": + scrollBarStyle = (Integer) value; + break; + case "verticalScrollbarPosition": + verticalScrollbarPosition = (Integer) value; + break; + case "scrollBarDefaultDelayBeforeFade": + scrollBarDefaultDelayBeforeFade = (Integer) value; + break; + case "scrollbarFadingEnabled": + scrollbarFadingEnabled = (Boolean) value; + break; + case "scrollBarFadeDuration": + scrollBarFadeDuration = (Integer) value; + break; + case "rendererPriorityPolicy": + rendererPriorityPolicy = (Map) value; + break; + case "useShouldInterceptRequest": + useShouldInterceptRequest = (Boolean) value; + break; + case "useOnRenderProcessGone": + useOnRenderProcessGone = (Boolean) value; + break; + case "disableDefaultErrorPage": + disableDefaultErrorPage = (Boolean) value; + break; + case "useHybridComposition": + useHybridComposition = (Boolean) value; + break; + case "verticalScrollbarThumbColor": + verticalScrollbarThumbColor = (String) value; + break; + case "verticalScrollbarTrackColor": + verticalScrollbarTrackColor = (String) value; + break; + case "horizontalScrollbarThumbColor": + horizontalScrollbarThumbColor = (String) value; + break; + case "horizontalScrollbarTrackColor": + horizontalScrollbarTrackColor = (String) value; + break; + case "algorithmicDarkeningAllowed": + algorithmicDarkeningAllowed = (Boolean) value; + break; + case "enterpriseAuthenticationAppLinkPolicyEnabled": + enterpriseAuthenticationAppLinkPolicyEnabled = (Boolean) value; + break; + case "allowBackgroundAudioPlaying": + allowBackgroundAudioPlaying = (Boolean) value; + break; + case "webViewAssetLoader": + webViewAssetLoader = (Map) value; + break; + case "defaultVideoPoster": + defaultVideoPoster = (byte[]) value; + break; + case "requestedWithHeaderOriginAllowList": + requestedWithHeaderOriginAllowList = new HashSet<>((List) value); + break; + } + } + + return this; + } + + @NonNull + @Override + public Map toMap() { + Map settings = new HashMap<>(); + settings.put("useShouldOverrideUrlLoading", useShouldOverrideUrlLoading); + settings.put("useOnLoadResource", useOnLoadResource); + settings.put("useOnDownloadStart", useOnDownloadStart); + settings.put("clearCache", clearCache); + settings.put("userAgent", userAgent); + settings.put("applicationNameForUserAgent", applicationNameForUserAgent); + settings.put("javaScriptEnabled", javaScriptEnabled); + settings.put("javaScriptCanOpenWindowsAutomatically", javaScriptCanOpenWindowsAutomatically); + settings.put("mediaPlaybackRequiresUserGesture", mediaPlaybackRequiresUserGesture); + settings.put("minimumFontSize", minimumFontSize); + settings.put("verticalScrollBarEnabled", verticalScrollBarEnabled); + settings.put("horizontalScrollBarEnabled", horizontalScrollBarEnabled); + settings.put("resourceCustomSchemes", resourceCustomSchemes); + settings.put("contentBlockers", contentBlockers); + settings.put("preferredContentMode", preferredContentMode); + settings.put("useShouldInterceptAjaxRequest", useShouldInterceptAjaxRequest); + settings.put("interceptOnlyAsyncAjaxRequests", interceptOnlyAsyncAjaxRequests); + settings.put("useShouldInterceptFetchRequest", useShouldInterceptFetchRequest); + settings.put("incognito", incognito); + settings.put("cacheEnabled", cacheEnabled); + settings.put("transparentBackground", transparentBackground); + settings.put("disableVerticalScroll", disableVerticalScroll); + settings.put("disableHorizontalScroll", disableHorizontalScroll); + settings.put("disableContextMenu", disableContextMenu); + settings.put("textZoom", textZoom); + settings.put("clearSessionCache", clearSessionCache); + settings.put("builtInZoomControls", builtInZoomControls); + settings.put("displayZoomControls", displayZoomControls); + settings.put("supportZoom", supportZoom); + settings.put("databaseEnabled", databaseEnabled); + settings.put("domStorageEnabled", domStorageEnabled); + settings.put("useWideViewPort", useWideViewPort); + settings.put("safeBrowsingEnabled", safeBrowsingEnabled); + settings.put("mixedContentMode", mixedContentMode); + settings.put("allowContentAccess", allowContentAccess); + settings.put("allowFileAccess", allowFileAccess); + settings.put("allowFileAccessFromFileURLs", allowFileAccessFromFileURLs); + settings.put("allowUniversalAccessFromFileURLs", allowUniversalAccessFromFileURLs); + settings.put("appCachePath", appCachePath); + settings.put("blockNetworkImage", blockNetworkImage); + settings.put("blockNetworkLoads", blockNetworkLoads); + settings.put("cacheMode", cacheMode); + settings.put("cursiveFontFamily", cursiveFontFamily); + settings.put("defaultFixedFontSize", defaultFixedFontSize); + settings.put("defaultFontSize", defaultFontSize); + settings.put("defaultTextEncodingName", defaultTextEncodingName); + settings.put("disabledActionModeMenuItems", disabledActionModeMenuItems); + settings.put("fantasyFontFamily", fantasyFontFamily); + settings.put("fixedFontFamily", fixedFontFamily); + settings.put("forceDark", forceDark); + settings.put("forceDarkStrategy", forceDarkStrategy); + settings.put("geolocationEnabled", geolocationEnabled); + settings.put("layoutAlgorithm", getLayoutAlgorithm()); + settings.put("loadWithOverviewMode", loadWithOverviewMode); + settings.put("loadsImagesAutomatically", loadsImagesAutomatically); + settings.put("minimumLogicalFontSize", minimumLogicalFontSize); + settings.put("initialScale", initialScale); + settings.put("needInitialFocus", needInitialFocus); + settings.put("offscreenPreRaster", offscreenPreRaster); + settings.put("sansSerifFontFamily", sansSerifFontFamily); + settings.put("serifFontFamily", serifFontFamily); + settings.put("standardFontFamily", standardFontFamily); + settings.put("saveFormData", saveFormData); + settings.put("thirdPartyCookiesEnabled", thirdPartyCookiesEnabled); + settings.put("hardwareAcceleration", hardwareAcceleration); + settings.put("supportMultipleWindows", supportMultipleWindows); + settings.put("regexToCancelSubFramesLoading", regexToCancelSubFramesLoading); + settings.put("overScrollMode", overScrollMode); + settings.put("networkAvailable", networkAvailable); + settings.put("scrollBarStyle", scrollBarStyle); + settings.put("verticalScrollbarPosition", verticalScrollbarPosition); + settings.put("scrollBarDefaultDelayBeforeFade", scrollBarDefaultDelayBeforeFade); + settings.put("scrollbarFadingEnabled", scrollbarFadingEnabled); + settings.put("scrollBarFadeDuration", scrollBarFadeDuration); + settings.put("rendererPriorityPolicy", rendererPriorityPolicy); + settings.put("useShouldInterceptRequest", useShouldInterceptRequest); + settings.put("useOnRenderProcessGone", useOnRenderProcessGone); + settings.put("disableDefaultErrorPage", disableDefaultErrorPage); + settings.put("useHybridComposition", useHybridComposition); + settings.put("verticalScrollbarThumbColor", verticalScrollbarThumbColor); + settings.put("verticalScrollbarTrackColor", verticalScrollbarTrackColor); + settings.put("horizontalScrollbarThumbColor", horizontalScrollbarThumbColor); + settings.put("horizontalScrollbarTrackColor", horizontalScrollbarTrackColor); + settings.put("algorithmicDarkeningAllowed", algorithmicDarkeningAllowed); + settings.put("enterpriseAuthenticationAppLinkPolicyEnabled", enterpriseAuthenticationAppLinkPolicyEnabled); + settings.put("allowBackgroundAudioPlaying", allowBackgroundAudioPlaying); + settings.put("defaultVideoPoster", defaultVideoPoster); + settings.put("requestedWithHeaderOriginAllowList", + requestedWithHeaderOriginAllowList != null ? new ArrayList<>(requestedWithHeaderOriginAllowList) : null); + return settings; + } + + @SuppressLint("RestrictedApi") + @NonNull + @Override + public Map getRealSettings(@NonNull InAppWebViewInterface inAppWebView) { + Map realSettings = toMap(); + if (inAppWebView instanceof InAppWebView) { + InAppWebView webView = (InAppWebView) inAppWebView; + WebSettings settings = webView.getSettings(); + realSettings.put("userAgent", settings.getUserAgentString()); + realSettings.put("javaScriptEnabled", settings.getJavaScriptEnabled()); + realSettings.put("javaScriptCanOpenWindowsAutomatically", settings.getJavaScriptCanOpenWindowsAutomatically()); + realSettings.put("mediaPlaybackRequiresUserGesture", settings.getMediaPlaybackRequiresUserGesture()); + realSettings.put("minimumFontSize", settings.getMinimumFontSize()); + realSettings.put("verticalScrollBarEnabled", webView.isVerticalScrollBarEnabled()); + realSettings.put("horizontalScrollBarEnabled", webView.isHorizontalScrollBarEnabled()); + realSettings.put("textZoom", settings.getTextZoom()); + realSettings.put("builtInZoomControls", settings.getBuiltInZoomControls()); + realSettings.put("supportZoom", settings.supportZoom()); + realSettings.put("displayZoomControls", settings.getDisplayZoomControls()); + realSettings.put("databaseEnabled", settings.getDatabaseEnabled()); + realSettings.put("domStorageEnabled", settings.getDomStorageEnabled()); + realSettings.put("useWideViewPort", settings.getUseWideViewPort()); + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) { + realSettings.put("safeBrowsingEnabled", WebSettingsCompat.getSafeBrowsingEnabled(settings)); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + realSettings.put("safeBrowsingEnabled", settings.getSafeBrowsingEnabled()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + realSettings.put("mixedContentMode", settings.getMixedContentMode()); + } + realSettings.put("allowContentAccess", settings.getAllowContentAccess()); + realSettings.put("allowFileAccess", settings.getAllowFileAccess()); + realSettings.put("allowFileAccessFromFileURLs", settings.getAllowFileAccessFromFileURLs()); + realSettings.put("allowUniversalAccessFromFileURLs", settings.getAllowUniversalAccessFromFileURLs()); + realSettings.put("blockNetworkImage", settings.getBlockNetworkImage()); + realSettings.put("blockNetworkLoads", settings.getBlockNetworkLoads()); + realSettings.put("cacheMode", settings.getCacheMode()); + realSettings.put("cursiveFontFamily", settings.getCursiveFontFamily()); + realSettings.put("defaultFixedFontSize", settings.getDefaultFixedFontSize()); + realSettings.put("defaultFontSize", settings.getDefaultFontSize()); + realSettings.put("defaultTextEncodingName", settings.getDefaultTextEncodingName()); + if (WebViewFeature.isFeatureSupported(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS)) { + realSettings.put("disabledActionModeMenuItems", WebSettingsCompat.getDisabledActionModeMenuItems(settings)); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + realSettings.put("disabledActionModeMenuItems", settings.getDisabledActionModeMenuItems()); + } + realSettings.put("fantasyFontFamily", settings.getFantasyFontFamily()); + realSettings.put("fixedFontFamily", settings.getFixedFontFamily()); + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + realSettings.put("forceDark", WebSettingsCompat.getForceDark(settings)); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + realSettings.put("forceDark", settings.getForceDark()); + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + realSettings.put("forceDarkStrategy", WebSettingsCompat.getForceDarkStrategy(settings)); + } + realSettings.put("layoutAlgorithm", settings.getLayoutAlgorithm().name()); + realSettings.put("loadWithOverviewMode", settings.getLoadWithOverviewMode()); + realSettings.put("loadsImagesAutomatically", settings.getLoadsImagesAutomatically()); + realSettings.put("minimumLogicalFontSize", settings.getMinimumLogicalFontSize()); + if (WebViewFeature.isFeatureSupported(WebViewFeature.OFF_SCREEN_PRERASTER)) { + realSettings.put("offscreenPreRaster", WebSettingsCompat.getOffscreenPreRaster(settings)); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + realSettings.put("offscreenPreRaster", settings.getOffscreenPreRaster()); + } + realSettings.put("sansSerifFontFamily", settings.getSansSerifFontFamily()); + realSettings.put("serifFontFamily", settings.getSerifFontFamily()); + realSettings.put("standardFontFamily", settings.getStandardFontFamily()); + realSettings.put("saveFormData", settings.getSaveFormData()); + realSettings.put("supportMultipleWindows", settings.supportMultipleWindows()); + realSettings.put("overScrollMode", webView.getOverScrollMode()); + realSettings.put("scrollBarStyle", webView.getScrollBarStyle()); + realSettings.put("verticalScrollbarPosition", webView.getVerticalScrollbarPosition()); + realSettings.put("scrollBarDefaultDelayBeforeFade", webView.getScrollBarDefaultDelayBeforeFade()); + realSettings.put("scrollbarFadingEnabled", webView.isScrollbarFadingEnabled()); + realSettings.put("scrollBarFadeDuration", webView.getScrollBarFadeDuration()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Map rendererPriorityPolicy = new HashMap<>(); + rendererPriorityPolicy.put("rendererRequestedPriority", webView.getRendererRequestedPriority()); + rendererPriorityPolicy.put("waivedWhenNotVisible", webView.getRendererPriorityWaivedWhenNotVisible()); + realSettings.put("rendererPriorityPolicy", rendererPriorityPolicy); + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + realSettings.put("algorithmicDarkeningAllowed", WebSettingsCompat.isAlgorithmicDarkeningAllowed(settings)); + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY)) { + realSettings.put("enterpriseAuthenticationAppLinkPolicyEnabled", WebSettingsCompat.getEnterpriseAuthenticationAppLinkPolicyEnabled(settings)); + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) { + realSettings.put("requestedWithHeaderOriginAllowList", new ArrayList<>(WebSettingsCompat.getRequestedWithHeaderOriginAllowList(settings))); + } + } + return realSettings; + } + + private void setLayoutAlgorithm(String value) { + if (value != null) { + switch (value) { + case "NARROW_COLUMNS": + layoutAlgorithm = NARROW_COLUMNS; + case "NORMAL": + layoutAlgorithm = NORMAL; + case "TEXT_AUTOSIZING": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING; + } else { + layoutAlgorithm = NORMAL; + } + break; + } + } + } + + private String getLayoutAlgorithm() { + if (layoutAlgorithm != null) { + switch (layoutAlgorithm) { + case NORMAL: + return "NORMAL"; + case TEXT_AUTOSIZING: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return "TEXT_AUTOSIZING"; + } else { + return "NORMAL"; + } + case NARROW_COLUMNS: + return "NARROW_COLUMNS"; + } + } + return null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InputAwareWebView.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InputAwareWebView.java new file mode 100644 index 0000000..e4c5e96 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InputAwareWebView.java @@ -0,0 +1,273 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import static android.content.Context.INPUT_METHOD_SERVICE; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.webkit.WebView; +import android.widget.ListPopupWindow; + +import androidx.annotation.Nullable; + +/** + * A WebView subclass that mirrors the same implementation hacks that the system WebView does in + * order to correctly create an InputConnection. + * + * These hacks are only needed in Android versions below N and exist to create an InputConnection + * on the WebView's dedicated input, or IME, thread. The majority of this proxying logic is in + * https://github.com/flutter/plugins/blob/main/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java + */ +public class InputAwareWebView extends WebView { + private static final String LOG_TAG = "InputAwareWebView"; + @Nullable + public View containerView; + private View threadedInputConnectionProxyView; + private ThreadedInputConnectionProxyAdapterView proxyAdapterView; + private boolean useHybridComposition = false; + + public InputAwareWebView(Context context, @Nullable View containerView, Boolean useHybridComposition) { + super(context); + this.containerView = containerView; + this.useHybridComposition = useHybridComposition == null ? false : useHybridComposition; + } + + public InputAwareWebView(Context context, AttributeSet attrs) { + super(context, attrs); + this.containerView = null; + } + + public InputAwareWebView(Context context) { + super(context); + this.containerView = null; + } + + public InputAwareWebView(Context context, AttributeSet attrs, int defaultStyle) { + super(context, attrs, defaultStyle); + this.containerView = null; + } + + public void setContainerView(View containerView) { + this.containerView = containerView; + + if (proxyAdapterView == null) { + return; + } + + Log.w(LOG_TAG, "The containerView has changed while the proxyAdapterView exists."); + if (containerView != null) { + setInputConnectionTarget(proxyAdapterView); + } + } + + /** + * Set our proxy adapter view to use its cached input connection instead of creating new ones. + * + *

This is used to avoid losing our input connection when the virtual display is resized. + */ + public void lockInputConnection() { + if (proxyAdapterView == null) { + return; + } + + proxyAdapterView.setLocked(true); + } + + /** Sets the proxy adapter view back to its default behavior. */ + public void unlockInputConnection() { + if (proxyAdapterView == null) { + return; + } + + proxyAdapterView.setLocked(false); + } + + /** Restore the original InputConnection, if needed. */ + void dispose() { + if (useHybridComposition) { + return; + } + resetInputConnection(); + } + + /** + * Creates an InputConnection from the IME thread when needed. + * + *

We only need to create a {@link ThreadedInputConnectionProxyAdapterView} and create an + * InputConnectionProxy on the IME thread when WebView is doing the same thing. So we rely on the + * system calling this method for WebView's proxy view in order to know when we need to create our + * own. + * + *

This method would normally be called for any View that used the InputMethodManager. We rely + * on flutter/engine filtering the calls we receive down to the ones in our hierarchy and the + * system WebView in order to know whether or not the system WebView expects an InputConnection on + * the IME thread. + */ + @Override + public boolean checkInputConnectionProxy(final View view) { + if (useHybridComposition) { + return super.checkInputConnectionProxy(view); + } + // Check to see if the view param is WebView's ThreadedInputConnectionProxyView. + View previousProxy = threadedInputConnectionProxyView; + threadedInputConnectionProxyView = view; + if (previousProxy == view) { + // This isn't a new ThreadedInputConnectionProxyView. Ignore it. + return super.checkInputConnectionProxy(view); + } + if (containerView == null) { + Log.e( + LOG_TAG, + "Can't create a proxy view because there's no container view. Text input may not work."); + return super.checkInputConnectionProxy(view); + } + + // We've never seen this before, so we make the assumption that this is WebView's + // ThreadedInputConnectionProxyView. We are making the assumption that the only view that could + // possibly be interacting with the IMM here is WebView's ThreadedInputConnectionProxyView. + proxyAdapterView = + new ThreadedInputConnectionProxyAdapterView( + /*containerView=*/ containerView, + /*targetView=*/ view, + /*imeHandler=*/ view.getHandler()); + setInputConnectionTarget(/*targetView=*/ proxyAdapterView); + return super.checkInputConnectionProxy(view); + } + + /** + * Ensure that input creation happens back on {@link #containerView}'s thread once this view no + * longer has focus. + * + *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's + * thread for all connections. We undo it here so users will be able to go back to typing in + * Flutter UIs as expected. + */ + @Override + public void clearFocus() { + super.clearFocus(); + + if (useHybridComposition) { + return; + } + resetInputConnection(); + } + + /** + * Ensure that input creation happens back on {@link #containerView}. + * + *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's + * thread for all connections. We undo it here so users will be able to go back to typing in + * Flutter UIs as expected. + */ + private void resetInputConnection() { + if (proxyAdapterView == null) { + // No need to reset the InputConnection to the default thread if we've never changed it. + return; + } + if (containerView == null) { + Log.e(LOG_TAG, "Can't reset the input connection to the container view because there is none."); + return; + } + setInputConnectionTarget(/*targetView=*/ containerView); + } + + /** + * This is the crucial trick that gets the InputConnection creation to happen on the correct + * thread pre Android N. + * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java?l=169&rcl=f0698ee3e4483fad5b0c34159276f71cfaf81f3a + * + *

{@code targetView} should have a {@link View#getHandler} method with the thread that future + * InputConnections should be created on. + */ + private void setInputConnectionTarget(final View targetView) { + if (containerView == null) { + Log.e( + LOG_TAG, + "Can't set the input connection target because there is no containerView to use as a handler."); + return; + } + + targetView.requestFocus(); + containerView.post( + new Runnable() { + @Override + public void run() { + if (containerView == null) { + Log.e( + LOG_TAG, + "Can't set the input connection target because there is no containerView to use as a handler."); + return; + } + + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); + // This is a hack to make InputMethodManager believe that the target view now has focus. + // As a result, InputMethodManager will think that targetView is focused, and will call + // getHandler() of the view when creating input connection. + + // Step 1: Set targetView as InputMethodManager#mNextServedView. This does not affect + // the real window focus. + targetView.onWindowFocusChanged(true); + + // Step 2: Have InputMethodManager focus in on targetView. As a result, IMM will call + // onCreateInputConnection() on targetView on the same thread as + // targetView.getHandler(). It will also call subsequent InputConnection methods on this + // thread. This is the IME thread in cases where targetView is our proxyAdapterView. + + // TODO (ALexVincent525): Currently only prompt has been tested, still needs more test cases. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + imm.isActive(containerView); + } + } + }); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (useHybridComposition) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + return; + } + // This works around a crash when old (<67.0.3367.0) Chromium versions are used. + + // Prior to Chromium 67.0.3367 the following sequence happens when a select drop down is shown + // on tablets: + // + // - WebView is calling ListPopupWindow#show + // - buildDropDown is invoked, which sets mDropDownList to a DropDownListView. + // - showAsDropDown is invoked - resulting in mDropDownList being added to the window and is + // also synchronously performing the following sequence: + // - WebView's focus change listener is loosing focus (as mDropDownList got it) + // - WebView is hiding all popups (as it lost focus) + // - WebView's SelectPopupDropDown#hide is invoked. + // - DropDownPopupWindow#dismiss is invoked setting mDropDownList to null. + // - mDropDownList#setSelection is invoked and is throwing a NullPointerException (as we just set mDropDownList to null). + // + // To workaround this, we drop the problematic focus lost call. + // See more details on: https://github.com/flutter/flutter/issues/54164 + // + // We don't do this after Android P as it shipped with a new enough WebView version, and it's + // better to not do this on all future Android versions in case DropDownListView's code changes. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P + && isCalledFromListPopupWindowShow() + && !focused) { + return; + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + private boolean isCalledFromListPopupWindowShow() { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + for (StackTraceElement stackTraceElement : stackTraceElements) { + if (stackTraceElement.getClassName().equals(ListPopupWindow.class.getCanonicalName()) + && stackTraceElement.getMethodName().equals("show")) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/ThreadedInputConnectionProxyAdapterView.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/ThreadedInputConnectionProxyAdapterView.java new file mode 100644 index 0000000..91a4546 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/ThreadedInputConnectionProxyAdapterView.java @@ -0,0 +1,99 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.os.Handler; +import android.os.IBinder; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +/** + * A fake View only exposed to InputMethodManager. + * + * https://github.com/flutter/plugins/blob/main/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java + */ +final class ThreadedInputConnectionProxyAdapterView extends View { + final Handler imeHandler; + final IBinder windowToken; + final View containerView; + final View rootView; + final View targetView; + + private boolean triggerDelayed = true; + private boolean isLocked = false; + private InputConnection cachedConnection; + + ThreadedInputConnectionProxyAdapterView(View containerView, View targetView, Handler imeHandler) { + super(containerView.getContext()); + this.imeHandler = imeHandler; + this.containerView = containerView; + this.targetView = targetView; + windowToken = containerView.getWindowToken(); + rootView = containerView.getRootView(); + setFocusable(true); + setFocusableInTouchMode(true); + setVisibility(VISIBLE); + } + + /** Returns whether or not this is currently asynchronously acquiring an input connection. */ + boolean isTriggerDelayed() { + return triggerDelayed; + } + + /** Sets whether or not this should use its previously cached input connection. */ + void setLocked(boolean locked) { + isLocked = locked; + } + + /** + * This is expected to be called on the IME thread. See the setup required for this in {@link + * InputAwareWebView#checkInputConnectionProxy(View)}. + * + *

Delegates to ThreadedInputConnectionProxyView to get WebView's input connection. + */ + @Override + public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { + triggerDelayed = false; + InputConnection inputConnection = + (isLocked) ? cachedConnection : targetView.onCreateInputConnection(outAttrs); + triggerDelayed = true; + cachedConnection = inputConnection; + return inputConnection; + } + + @Override + public boolean checkInputConnectionProxy(View view) { + return true; + } + + @Override + public boolean hasWindowFocus() { + // None of our views here correctly report they have window focus because of how we're embedding + // the platform view inside of a virtual display. + return true; + } + + @Override + public View getRootView() { + return rootView; + } + + @Override + public boolean onCheckIsTextEditor() { + return true; + } + + @Override + public boolean isFocused() { + return true; + } + + @Override + public IBinder getWindowToken() { + return windowToken; + } + + @Override + public Handler getHandler() { + return imeHandler; + } +} \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannel.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannel.java new file mode 100644 index 0000000..b7d059c --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannel.java @@ -0,0 +1,166 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.web_message; + +import android.webkit.ValueCallback; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebMessagePortCompat; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessageCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessagePortCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessagePort; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +public class WebMessageChannel implements Disposable { + protected static final String LOG_TAG = "WebMessageChannel"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_channel_"; + + @NonNull + public String id; + @Nullable + public WebMessageChannelChannelDelegate channelDelegate; + public final List compatPorts; + public final List ports; + @Nullable + public InAppWebViewInterface webView; + + public WebMessageChannel(@NonNull String id, @NonNull InAppWebViewInterface webView) { + this.id = id; + final MethodChannel channel = new MethodChannel(webView.getPlugin().messenger, METHOD_CHANNEL_NAME_PREFIX + id); + this.channelDelegate = new WebMessageChannelChannelDelegate(this, channel); + if (webView instanceof InAppWebView) { + this.compatPorts = new ArrayList<>(Arrays.asList(WebViewCompat.createWebMessageChannel((InAppWebView) webView))); + this.ports = new ArrayList<>(); + } else { + this.ports = Arrays.asList(new WebMessagePort("port1", this), new WebMessagePort("port2", this)); + this.compatPorts = new ArrayList<>(); + } + this.webView = webView; + } + + public void initJsInstance(InAppWebViewInterface webView, final ValueCallback callback) { + if (webView != null) { + final WebMessageChannel webMessageChannel = this; + webView.evaluateJavascript("(function() {" + + JavaScriptBridgeJS.WEB_MESSAGE_CHANNELS_VARIABLE_NAME + "['" + webMessageChannel.id + "'] = new MessageChannel();" + + "})();", null, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + callback.onReceiveValue(webMessageChannel); + } + }); + } else { + callback.onReceiveValue(this); + } + } + + public void setWebMessageCallbackForInAppWebView(final int index, @NonNull MethodChannel.Result result) { + if (webView != null && compatPorts.size() > 0 && + WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK)) { + final WebMessagePortCompat webMessagePort = compatPorts.get(index); + final WebMessageChannel webMessageChannel = this; + try { + webMessagePort.setWebMessageCallback(new WebMessagePortCompat.WebMessageCallbackCompat() { + @Override + public void onMessage(@NonNull WebMessagePortCompat port, @Nullable WebMessageCompat message) { + super.onMessage(port, message); + webMessageChannel.onMessage(index, message != null ? WebMessageCompatExt.fromMapWebMessageCompat(message) : null); + } + }); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + } + + public void postMessageForInAppWebView(final Integer index, @NonNull WebMessageCompatExt message, @NonNull MethodChannel.Result result) { + if (webView != null && compatPorts.size() > 0 && + WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE)) { + WebMessagePortCompat port = compatPorts.get(index); + List webMessagePorts = new ArrayList<>(); + List portsExt = message.getPorts(); + if (portsExt != null) { + for (WebMessagePortCompatExt portExt : portsExt) { + WebMessageChannel webMessageChannel = webView.getWebMessageChannels().get(portExt.getWebMessageChannelId()); + if (webMessageChannel != null) { + webMessagePorts.add(webMessageChannel.compatPorts.get(portExt.getIndex())); + } + } + } + Object data = message.getData(); + try { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && data != null && + message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + port.postMessage(new WebMessageCompat((byte[]) data, webMessagePorts.toArray(new WebMessagePortCompat[0]))); + } else { + port.postMessage(new WebMessageCompat(data != null ? data.toString() : null, webMessagePorts.toArray(new WebMessagePortCompat[0]))); + } + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + } + + public void closeForInAppWebView(Integer index, @NonNull MethodChannel.Result result) { + if (webView != null && compatPorts.size() > 0 && + WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_CLOSE)) { + WebMessagePortCompat port = compatPorts.get(index); + try { + port.close(); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + } + + public void onMessage(int index, @Nullable WebMessageCompatExt message) { + if (channelDelegate != null) { + channelDelegate.onMessage(index, message); + } + } + + public Map toMap() { + Map webMessageChannelMap = new HashMap<>(); + webMessageChannelMap.put("id", id); + return webMessageChannelMap; + } + + public void dispose() { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_CLOSE)) { + for (WebMessagePortCompat port : compatPorts) { + try { + port.close(); + } catch (Exception ignored) {} + } + } + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + compatPorts.clear(); + webView = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannelChannelDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannelChannelDelegate.java new file mode 100644 index 0000000..f801a82 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannelChannelDelegate.java @@ -0,0 +1,72 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.web_message; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessageCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class WebMessageChannelChannelDelegate extends ChannelDelegateImpl { + @Nullable + private WebMessageChannel webMessageChannel; + + public WebMessageChannelChannelDelegate(@NonNull WebMessageChannel webMessageChannel, @NonNull MethodChannel channel) { + super(channel); + this.webMessageChannel = webMessageChannel; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + switch (call.method) { + case "setWebMessageCallback": + if (webMessageChannel != null && webMessageChannel.webView instanceof InAppWebView) { + final Integer index = (Integer) call.argument("index"); + webMessageChannel.setWebMessageCallbackForInAppWebView(index, result); + } else { + result.success(false); + } + break; + case "postMessage": + if (webMessageChannel != null && webMessageChannel.webView instanceof InAppWebView) { + final Integer index = (Integer) call.argument("index"); + WebMessageCompatExt message = WebMessageCompatExt.fromMap((Map) call.argument("message")); + webMessageChannel.postMessageForInAppWebView(index, message, result); + } else { + result.success(false); + } + break; + case "close": + if (webMessageChannel != null && webMessageChannel.webView instanceof InAppWebView) { + Integer index = (Integer) call.argument("index"); + webMessageChannel.closeForInAppWebView(index, result); + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + public void onMessage(int index, @Nullable WebMessageCompatExt message) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("index", index); + obj.put("message", message != null ? message.toMap() : null); + channel.invokeMethod("onMessage", obj); + } + + @Override + public void dispose() { + super.dispose(); + webMessageChannel = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListener.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListener.java new file mode 100644 index 0000000..fe7326f --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListener.java @@ -0,0 +1,241 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.web_message; + +import android.net.Uri; +import android.text.TextUtils; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.JavaScriptReplyProxy; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessageCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface; +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; + +public class WebMessageListener implements Disposable { + protected static final String LOG_TAG = "WebMessageListener"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_listener_"; + + @NonNull + public String id; + public String jsObjectName; + public Set allowedOriginRules; + public WebViewCompat.WebMessageListener listener; + public JavaScriptReplyProxy replyProxy; + @Nullable + public InAppWebViewInterface webView; + @Nullable + public WebMessageListenerChannelDelegate channelDelegate; + + public WebMessageListener(@NonNull String id, + @NonNull InAppWebViewInterface webView, @NonNull BinaryMessenger messenger, + @NonNull String jsObjectName, @NonNull Set allowedOriginRules) { + this.id = id; + this.webView = webView; + this.jsObjectName = jsObjectName; + this.allowedOriginRules = allowedOriginRules; + final MethodChannel channel = new MethodChannel(messenger, METHOD_CHANNEL_NAME_PREFIX + this.id + "_" + this.jsObjectName); + this.channelDelegate = new WebMessageListenerChannelDelegate(this, channel); + + if (this.webView instanceof InAppWebView) { + this.listener = new WebViewCompat.WebMessageListener() { + @Override + public void onPostMessage(@NonNull WebView view, @NonNull WebMessageCompat message, @NonNull Uri sourceOrigin, + boolean isMainFrame, @NonNull JavaScriptReplyProxy javaScriptReplyProxy) { + replyProxy = javaScriptReplyProxy; + if (channelDelegate != null) { + channelDelegate.onPostMessage(WebMessageCompatExt.fromMapWebMessageCompat(message), + sourceOrigin.toString().equals("null") ? null : sourceOrigin.toString(), + isMainFrame); + } + } + }; + } + } + + public void initJsInstance() { + if (webView != null) { + String jsObjectNameEscaped = Util.replaceAll(jsObjectName, "\'", "\\'"); + List allowedOriginRulesStringList = new ArrayList<>(); + for (String allowedOriginRule : allowedOriginRules) { + if ("*".equals(allowedOriginRule)) { + allowedOriginRulesStringList.add("'*'"); + } else { + Uri rule = Uri.parse(allowedOriginRule); + String host = rule.getHost() != null ? "'" + Util.replaceAll(rule.getHost(), "\'", "\\'") + "'" : "null"; + allowedOriginRulesStringList.add("{scheme: '" + rule.getScheme() + "', host: " + host + ", port: " + (rule.getPort() != -1 ? rule.getPort() : "null") + "}"); + } + } + String allowedOriginRulesString = TextUtils.join(", ", allowedOriginRulesStringList); + + String source = "(function() {" + + " var allowedOriginRules = [" + allowedOriginRulesString + "];" + + " var isPageBlank = window.location.href === 'about:blank';" + + " var scheme = !isPageBlank ? window.location.protocol.replace(':', '') : null;" + + " var host = !isPageBlank ? window.location.hostname : null;" + + " var port = !isPageBlank ? window.location.port : null;" + + " if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._isOriginAllowed(allowedOriginRules, scheme, host, port)) {" + + " window['" + jsObjectNameEscaped + "'] = new FlutterInAppWebViewWebMessageListener('" + jsObjectNameEscaped + "');" + + " }" + + "})();"; + webView.getUserContentController().addPluginScript(new PluginScript( + "WebMessageListener-" + jsObjectName, + source, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false, + null + )); + } + } + + @Nullable + public static WebMessageListener fromMap(@NonNull InAppWebViewInterface webView, @NonNull BinaryMessenger messenger, @Nullable Map map) { + if (map == null) { + return null; + } + String id = (String) map.get("id"); + String jsObjectName = (String) map.get("jsObjectName"); + assert jsObjectName != null; + List allowedOriginRuleList = (List) map.get("allowedOriginRules"); + assert allowedOriginRuleList != null; + Set allowedOriginRules = new HashSet<>(allowedOriginRuleList); + return new WebMessageListener(id, webView, messenger, jsObjectName, allowedOriginRules); + } + + public void assertOriginRulesValid() throws Exception { + int index = 0; + for (String originRule : allowedOriginRules) { + if (originRule == null) { + throw new Exception("allowedOriginRules[" + index + "] is null"); + } + if (originRule.isEmpty()) { + throw new Exception("allowedOriginRules[" + index + "] is empty"); + } + if ("*".equals(originRule)) { + continue; + } + Uri url = Uri.parse(originRule); + String scheme = url.getScheme(); + String host = url.getHost(); + String path = url.getPath(); + int port = url.getPort(); + if (scheme == null) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if (("http".equals(scheme) || "https".equals(scheme)) && (host == null || host.isEmpty())) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if (!"http".equals(scheme) && !"https".equals(scheme) && (host != null || port != -1)) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if ((host == null || host.isEmpty()) && port != -1) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if (!path.isEmpty()) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if (host != null) { + int distance = host.indexOf("*"); + if (distance != 0 || (distance == 0 && !host.startsWith("*."))) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if (host.startsWith("[")) { + if (!host.endsWith("]")) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + String ipv6 = host.substring(1, host.length() - 1); + if (!Util.isIPv6(ipv6)) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + } + } + index++; + } + } + + public void postMessageForInAppWebView(WebMessageCompatExt message, @NonNull MethodChannel.Result result) { + if (replyProxy != null && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + Object data = message.getData(); + if (data != null) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + replyProxy.postMessage((byte[]) data); + } else { + replyProxy.postMessage(data.toString()); + } + } + } + result.success(true); + } + + public boolean isOriginAllowed(String scheme, String host, int port) { + for (String allowedOriginRule : allowedOriginRules) { + if ("*".equals(allowedOriginRule)) { + return true; + } + if (scheme == null || scheme.isEmpty()) { + continue; + } + if ((scheme == null || scheme.isEmpty()) && (host == null || host.isEmpty()) && (port == 0 || port == -1)) { + continue; + } + Uri rule = Uri.parse(allowedOriginRule); + int rulePort = rule.getPort() == -1 || rule.getPort() == 0 ? ("https".equals(rule.getScheme()) ? 443 : 80) : rule.getPort(); + int currentPort = port == 0 || port == -1 ? ("https".equals(scheme) ? 443 : 80) : port; + String IPv6 = null; + if (rule.getHost() != null && rule.getHost().startsWith("[")) { + try { + IPv6 = Util.normalizeIPv6(rule.getHost().substring(1, rule.getHost().length() - 1)); + } catch (Exception ignored) { + } + } + String hostIPv6 = null; + try { + hostIPv6 = Util.normalizeIPv6(host); + } catch (Exception ignored) { + } + + boolean schemeAllowed = rule.getScheme().equals(scheme); + + boolean hostAllowed = rule.getHost() == null || + rule.getHost().isEmpty() || + rule.getHost().equals(host) || + (rule.getHost().startsWith("*") && host != null && host.contains(rule.getHost().split("\\*")[1])) || + (hostIPv6 != null && IPv6 != null && hostIPv6.equals(IPv6)); + + boolean portAllowed = rulePort == currentPort; + + if (schemeAllowed && hostAllowed && portAllowed) { + return true; + } + } + return false; + } + + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + listener = null; + replyProxy = null; + webView = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListenerChannelDelegate.java b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListenerChannelDelegate.java new file mode 100644 index 0000000..92c7aaa --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListenerChannelDelegate.java @@ -0,0 +1,56 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.web_message; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessageCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class WebMessageListenerChannelDelegate extends ChannelDelegateImpl { + @Nullable + private WebMessageListener webMessageListener; + + public WebMessageListenerChannelDelegate(@NonNull WebMessageListener webMessageListener, @NonNull MethodChannel channel) { + super(channel); + this.webMessageListener = webMessageListener; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + switch (call.method) { + case "postMessage": + if (webMessageListener != null && webMessageListener.webView instanceof InAppWebView) { + WebMessageCompatExt message = WebMessageCompatExt.fromMap((Map) call.argument("message")); + webMessageListener.postMessageForInAppWebView(message, result); + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + public void onPostMessage(@Nullable WebMessageCompatExt message, String sourceOrigin, boolean isMainFrame) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("message", message != null ? message.toMap() : null); + obj.put("sourceOrigin", sourceOrigin); + obj.put("isMainFrame", isMainFrame); + channel.invokeMethod("onPostMessage", obj); + } + + @Override + public void dispose() { + super.dispose(); + webMessageListener = null; + } +} diff --git a/packages/flutter_inappwebview_android/android/src/main/res/drawable/floating_action_mode_shape.xml b/packages/flutter_inappwebview_android/android/src/main/res/drawable/floating_action_mode_shape.xml new file mode 100644 index 0000000..9ad086b --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/res/drawable/floating_action_mode_shape.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/res/layout/activity_web_view.xml b/packages/flutter_inappwebview_android/android/src/main/res/layout/activity_web_view.xml new file mode 100644 index 0000000..d7d7e21 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/res/layout/activity_web_view.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/packages/flutter_inappwebview_android/android/src/main/res/layout/chrome_custom_tabs_layout.xml b/packages/flutter_inappwebview_android/android/src/main/res/layout/chrome_custom_tabs_layout.xml new file mode 100644 index 0000000..f9504c9 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/res/layout/chrome_custom_tabs_layout.xml @@ -0,0 +1,6 @@ + + + + diff --git a/packages/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode.xml b/packages/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode.xml new file mode 100644 index 0000000..b58cc7c --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode_item.xml b/packages/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode_item.xml new file mode 100644 index 0000000..3ce3c11 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode_item.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/android/src/main/res/menu/menu_main.xml b/packages/flutter_inappwebview_android/android/src/main/res/menu/menu_main.xml new file mode 100644 index 0000000..2bff50b --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/res/menu/menu_main.xml @@ -0,0 +1,49 @@ + +

+ + + + + + + + + + + + + + diff --git a/packages/flutter_inappwebview_android/android/src/main/res/values/strings.xml b/packages/flutter_inappwebview_android/android/src/main/res/values/strings.xml new file mode 100644 index 0000000..9301a93 --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + Go Back + Go Forward + Reload + Share + Close + Search + diff --git a/packages/flutter_inappwebview_android/android/src/main/res/values/styles.xml b/packages/flutter_inappwebview_android/android/src/main/res/values/styles.xml new file mode 100644 index 0000000..13b55bd --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/res/values/styles.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/packages/flutter_inappwebview_android/android/src/main/res/xml/provider_paths.xml b/packages/flutter_inappwebview_android/android/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..ffa74ab --- /dev/null +++ b/packages/flutter_inappwebview_android/android/src/main/res/xml/provider_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/flutter_inappwebview_android/build.yaml b/packages/flutter_inappwebview_android/build.yaml new file mode 100644 index 0000000..e2b3acf --- /dev/null +++ b/packages/flutter_inappwebview_android/build.yaml @@ -0,0 +1,5 @@ +targets: + $default: + sources: + exclude: + - example/**.dart diff --git a/packages/flutter_inappwebview_android/lib/flutter_inappwebview_android.dart b/packages/flutter_inappwebview_android/lib/flutter_inappwebview_android.dart new file mode 100644 index 0000000..dd86c9e --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/flutter_inappwebview_android.dart @@ -0,0 +1,3 @@ +library flutter_inappwebview_android; + +export 'src/main.dart'; diff --git a/packages/flutter_inappwebview_android/lib/src/chrome_safari_browser/chrome_safari_browser.dart b/packages/flutter_inappwebview_android/lib/src/chrome_safari_browser/chrome_safari_browser.dart new file mode 100644 index 0000000..18336a4 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -0,0 +1,376 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidChromeSafariBrowser]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformChromeSafariBrowserCreationParams] for +/// more information. +@immutable +class AndroidChromeSafariBrowserCreationParams + extends PlatformChromeSafariBrowserCreationParams { + /// Creates a new [AndroidChromeSafariBrowserCreationParams] instance. + const AndroidChromeSafariBrowserCreationParams(); + + /// Creates a [AndroidChromeSafariBrowserCreationParams] instance based on [PlatformChromeSafariBrowserCreationParams]. + factory AndroidChromeSafariBrowserCreationParams.fromPlatformChromeSafariBrowserCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformChromeSafariBrowserCreationParams params) { + return AndroidChromeSafariBrowserCreationParams(); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformChromeSafariBrowser} +class AndroidChromeSafariBrowser extends PlatformChromeSafariBrowser + with ChannelController { + @override + final String id = IdGenerator.generate(); + + /// Constructs a [AndroidChromeSafariBrowser]. + AndroidChromeSafariBrowser(PlatformChromeSafariBrowserCreationParams params) + : super.implementation( + params is AndroidChromeSafariBrowserCreationParams + ? params + : AndroidChromeSafariBrowserCreationParams + .fromPlatformChromeSafariBrowserCreationParams(params), + ); + + static final AndroidChromeSafariBrowser _staticValue = + AndroidChromeSafariBrowser(AndroidChromeSafariBrowserCreationParams()); + + /// Provide static access. + factory AndroidChromeSafariBrowser.static() { + return _staticValue; + } + + ChromeSafariBrowserActionButton? _actionButton; + Map _menuItems = new HashMap(); + ChromeSafariBrowserSecondaryToolbar? _secondaryToolbar; + bool _isOpened = false; + static const MethodChannel _staticChannel = + const MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser'); + + _init() { + channel = + MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + id: id, + debugLoggingSettings: PlatformChromeSafariBrowser.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onServiceConnected": + eventHandler?.onServiceConnected(); + break; + case "onOpened": + eventHandler?.onOpened(); + break; + case "onCompletedInitialLoad": + final bool? didLoadSuccessfully = call.arguments["didLoadSuccessfully"]; + eventHandler?.onCompletedInitialLoad(didLoadSuccessfully); + break; + case "onNavigationEvent": + final navigationEvent = CustomTabsNavigationEventType.fromNativeValue( + call.arguments["navigationEvent"]); + eventHandler?.onNavigationEvent(navigationEvent); + break; + case "onRelationshipValidationResult": + final relation = + CustomTabsRelationType.fromNativeValue(call.arguments["relation"]); + final requestedOrigin = call.arguments["requestedOrigin"] != null + ? WebUri(call.arguments["requestedOrigin"]) + : null; + final bool result = call.arguments["result"]; + eventHandler?.onRelationshipValidationResult( + relation, requestedOrigin, result); + break; + case "onClosed": + _isOpened = false; + final onClosed = eventHandler?.onClosed; + dispose(); + onClosed?.call(); + break; + case "onItemActionPerform": + String url = call.arguments["url"]; + String title = call.arguments["title"]; + int id = call.arguments["id"].toInt(); + if (this._actionButton?.id == id) { + if (this._actionButton?.action != null) { + this._actionButton?.action!(url, title); + } + if (this._actionButton?.onClick != null) { + this._actionButton?.onClick!(WebUri(url), title); + } + } else if (this._menuItems[id] != null) { + if (this._menuItems[id]?.action != null) { + this._menuItems[id]?.action!(url, title); + } + if (this._menuItems[id]?.onClick != null) { + this._menuItems[id]?.onClick!(WebUri(url), title); + } + } + break; + case "onSecondaryItemActionPerform": + final clickableIDs = this._secondaryToolbar?.clickableIDs; + if (clickableIDs != null) { + WebUri? url = call.arguments["url"] != null + ? WebUri(call.arguments["url"]) + : null; + String name = call.arguments["name"]; + for (final clickable in clickableIDs) { + var clickableFullname = clickable.id.name; + if (clickable.id.defType != null && + !clickableFullname.contains("/")) { + clickableFullname = "${clickable.id.defType}/$clickableFullname"; + } + if (clickable.id.defPackage != null && + !clickableFullname.contains(":")) { + clickableFullname = + "${clickable.id.defPackage}:$clickableFullname"; + } + if (clickableFullname == name) { + if (clickable.onClick != null) { + clickable.onClick!(url); + } + break; + } + } + } + break; + case "onMessageChannelReady": + eventHandler?.onMessageChannelReady(); + break; + case "onPostMessage": + final String message = call.arguments["message"]; + eventHandler?.onPostMessage(message); + break; + case "onVerticalScrollEvent": + final bool isDirectionUp = call.arguments["isDirectionUp"]; + eventHandler?.onVerticalScrollEvent(isDirectionUp); + break; + case "onGreatestScrollPercentageIncreased": + final int scrollPercentage = call.arguments["scrollPercentage"]; + eventHandler?.onGreatestScrollPercentageIncreased(scrollPercentage); + break; + case "onSessionEnded": + final bool didUserInteract = call.arguments["didUserInteract"]; + eventHandler?.onSessionEnded(didUserInteract); + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Future open( + {WebUri? url, + Map? headers, + List? otherLikelyURLs, + WebUri? referrer, + @Deprecated('Use settings instead') + // ignore: deprecated_member_use_from_same_package + ChromeSafariBrowserClassOptions? options, + ChromeSafariBrowserSettings? settings}) async { + assert(!_isOpened, 'The browser is already opened.'); + _isOpened = true; + + if (Util.isIOS) { + assert(url != null, 'The specified URL must not be null on iOS.'); + assert(['http', 'https'].contains(url!.scheme), + 'The specified URL has an unsupported scheme. Only HTTP and HTTPS URLs are supported on iOS.'); + } + if (url != null) { + assert(url.toString().isNotEmpty, 'The specified URL must not be empty.'); + } + + _init(); + + List> menuItemList = []; + _menuItems.forEach((key, value) { + menuItemList.add(value.toMap()); + }); + + var initialSettings = settings?.toMap() ?? + options?.toMap() ?? + ChromeSafariBrowserSettings().toMap(); + + Map args = {}; + args.putIfAbsent('id', () => id); + args.putIfAbsent('url', () => url?.toString()); + args.putIfAbsent('headers', () => headers); + args.putIfAbsent('otherLikelyURLs', + () => otherLikelyURLs?.map((e) => e.toString()).toList()); + args.putIfAbsent('referrer', () => referrer?.toString()); + args.putIfAbsent('settings', () => initialSettings); + args.putIfAbsent('actionButton', () => _actionButton?.toMap()); + args.putIfAbsent('secondaryToolbar', () => _secondaryToolbar?.toMap()); + args.putIfAbsent('menuItemList', () => menuItemList); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future launchUrl({ + required WebUri url, + Map? headers, + List? otherLikelyURLs, + WebUri? referrer, + }) async { + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('headers', () => headers); + args.putIfAbsent('otherLikelyURLs', + () => otherLikelyURLs?.map((e) => e.toString()).toList()); + args.putIfAbsent('referrer', () => referrer?.toString()); + await channel?.invokeMethod("launchUrl", args); + } + + @override + Future mayLaunchUrl( + {WebUri? url, List? otherLikelyURLs}) async { + Map args = {}; + args.putIfAbsent('url', () => url?.toString()); + args.putIfAbsent('otherLikelyURLs', + () => otherLikelyURLs?.map((e) => e.toString()).toList()); + return await channel?.invokeMethod("mayLaunchUrl", args) ?? false; + } + + @override + Future validateRelationship( + {required CustomTabsRelationType relation, + required WebUri origin}) async { + Map args = {}; + args.putIfAbsent('relation', () => relation.toNativeValue()); + args.putIfAbsent('origin', () => origin.toString()); + return await channel?.invokeMethod("validateRelationship", args) ?? + false; + } + + @override + Future close() async { + Map args = {}; + await channel?.invokeMethod("close", args); + } + + @override + void setActionButton(ChromeSafariBrowserActionButton actionButton) { + this._actionButton = actionButton; + } + + @override + Future updateActionButton( + {required Uint8List icon, required String description}) async { + Map args = {}; + args.putIfAbsent('icon', () => icon); + args.putIfAbsent('description', () => description); + await channel?.invokeMethod("updateActionButton", args); + _actionButton?.icon = icon; + _actionButton?.description = description; + } + + @override + void setSecondaryToolbar( + ChromeSafariBrowserSecondaryToolbar secondaryToolbar) { + this._secondaryToolbar = secondaryToolbar; + } + + @override + Future updateSecondaryToolbar( + ChromeSafariBrowserSecondaryToolbar secondaryToolbar) async { + Map args = {}; + args.putIfAbsent('secondaryToolbar', () => secondaryToolbar.toMap()); + await channel?.invokeMethod("updateSecondaryToolbar", args); + this._secondaryToolbar = secondaryToolbar; + } + + @override + void addMenuItem(ChromeSafariBrowserMenuItem menuItem) { + this._menuItems[menuItem.id] = menuItem; + } + + @override + void addMenuItems(List menuItems) { + menuItems.forEach((menuItem) { + this._menuItems[menuItem.id] = menuItem; + }); + } + + @override + Future requestPostMessageChannel( + {required WebUri sourceOrigin, WebUri? targetOrigin}) async { + Map args = {}; + args.putIfAbsent("sourceOrigin", () => sourceOrigin.toString()); + args.putIfAbsent("targetOrigin", () => targetOrigin.toString()); + return await channel?.invokeMethod( + "requestPostMessageChannel", args) ?? + false; + } + + @override + Future postMessage(String message) async { + Map args = {}; + args.putIfAbsent("message", () => message); + return CustomTabsPostMessageResultType.fromNativeValue( + await channel?.invokeMethod("postMessage", args)) ?? + CustomTabsPostMessageResultType.FAILURE_MESSAGING_ERROR; + } + + @override + Future isEngagementSignalsApiAvailable() async { + Map args = {}; + return await channel?.invokeMethod( + "isEngagementSignalsApiAvailable", args) ?? + false; + } + + @override + Future isAvailable() async { + Map args = {}; + return await _staticChannel.invokeMethod("isAvailable", args) ?? + false; + } + + @override + Future getMaxToolbarItems() async { + Map args = {}; + return await _staticChannel.invokeMethod("getMaxToolbarItems", args) ?? + 0; + } + + @override + Future getPackageName( + {List? packages, bool ignoreDefault = false}) async { + Map args = {}; + args.putIfAbsent("packages", () => packages); + args.putIfAbsent("ignoreDefault", () => ignoreDefault); + return await _staticChannel.invokeMethod("getPackageName", args); + } + + @override + bool isOpened() { + return _isOpened; + } + + @override + @mustCallSuper + void dispose() { + super.dispose(); + disposeChannel(); + } +} diff --git a/packages/flutter_inappwebview_android/lib/src/chrome_safari_browser/main.dart b/packages/flutter_inappwebview_android/lib/src/chrome_safari_browser/main.dart new file mode 100644 index 0000000..9a6238b --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/chrome_safari_browser/main.dart @@ -0,0 +1 @@ +export 'chrome_safari_browser.dart'; diff --git a/packages/flutter_inappwebview_android/lib/src/cookie_manager.dart b/packages/flutter_inappwebview_android/lib/src/cookie_manager.dart new file mode 100644 index 0000000..c0e2fba --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/cookie_manager.dart @@ -0,0 +1,220 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidCookieManager]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformCookieManagerCreationParams] for +/// more information. +@immutable +class AndroidCookieManagerCreationParams + extends PlatformCookieManagerCreationParams { + /// Creates a new [AndroidCookieManagerCreationParams] instance. + const AndroidCookieManagerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformCookieManagerCreationParams params, + ) : super(); + + /// Creates a [AndroidCookieManagerCreationParams] instance based on [PlatformCookieManagerCreationParams]. + factory AndroidCookieManagerCreationParams.fromPlatformCookieManagerCreationParams( + PlatformCookieManagerCreationParams params) { + return AndroidCookieManagerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformCookieManager} +class AndroidCookieManager extends PlatformCookieManager + with ChannelController { + /// Creates a new [AndroidCookieManager]. + AndroidCookieManager(PlatformCookieManagerCreationParams params) + : super.implementation( + params is AndroidCookieManagerCreationParams + ? params + : AndroidCookieManagerCreationParams + .fromPlatformCookieManagerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_cookiemanager'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidCookieManager? _instance; + + ///Gets the [AndroidCookieManager] shared instance. + static AndroidCookieManager instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidCookieManager _init() { + _instance = AndroidCookieManager(AndroidCookieManagerCreationParams( + const PlatformCookieManagerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future setCookie( + {required WebUri url, + required String name, + required String value, + String path = "/", + String? domain, + int? expiresDate, + int? maxAge, + bool? isSecure, + bool? isHttpOnly, + HTTPCookieSameSitePolicy? sameSite, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + assert(value.isNotEmpty); + assert(path.isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('name', () => name); + args.putIfAbsent('value', () => value); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + args.putIfAbsent('expiresDate', () => expiresDate?.toString()); + args.putIfAbsent('maxAge', () => maxAge); + args.putIfAbsent('isSecure', () => isSecure); + args.putIfAbsent('isHttpOnly', () => isHttpOnly); + args.putIfAbsent('sameSite', () => sameSite?.toNativeValue()); + + return await channel?.invokeMethod('setCookie', args) ?? false; + } + + @override + Future> getCookies( + {required WebUri url, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + + List cookies = []; + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + List cookieListMap = + await channel?.invokeMethod('getCookies', args) ?? []; + cookieListMap = cookieListMap.cast>(); + + cookieListMap.forEach((cookieMap) { + cookies.add(Cookie( + name: cookieMap["name"], + value: cookieMap["value"], + expiresDate: cookieMap["expiresDate"], + isSessionOnly: cookieMap["isSessionOnly"], + domain: cookieMap["domain"], + sameSite: + HTTPCookieSameSitePolicy.fromNativeValue(cookieMap["sameSite"]), + isSecure: cookieMap["isSecure"], + isHttpOnly: cookieMap["isHttpOnly"], + path: cookieMap["path"])); + }); + return cookies; + } + + @override + Future getCookie( + {required WebUri url, + required String name, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + List cookies = + await channel?.invokeMethod('getCookies', args) ?? []; + cookies = cookies.cast>(); + for (var i = 0; i < cookies.length; i++) { + cookies[i] = cookies[i].cast(); + if (cookies[i]["name"] == name) + return Cookie( + name: cookies[i]["name"], + value: cookies[i]["value"], + expiresDate: cookies[i]["expiresDate"], + isSessionOnly: cookies[i]["isSessionOnly"], + domain: cookies[i]["domain"], + sameSite: HTTPCookieSameSitePolicy.fromNativeValue( + cookies[i]["sameSite"]), + isSecure: cookies[i]["isSecure"], + isHttpOnly: cookies[i]["isHttpOnly"], + path: cookies[i]["path"]); + } + return null; + } + + @override + Future deleteCookie( + {required WebUri url, + required String name, + String path = "/", + String? domain, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('name', () => name); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + return await channel?.invokeMethod('deleteCookie', args) ?? false; + } + + @override + Future deleteCookies( + {required WebUri url, + String path = "/", + String? domain, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + return await channel?.invokeMethod('deleteCookies', args) ?? false; + } + + @override + Future deleteAllCookies() async { + Map args = {}; + return await channel?.invokeMethod('deleteAllCookies', args) ?? false; + } + + @override + Future removeSessionCookies() async { + Map args = {}; + return await channel?.invokeMethod('removeSessionCookies', args) ?? + false; + } + + @override + void dispose() { + // empty + } +} + +extension InternalCookieManager on AndroidCookieManager { + get handleMethod => _handleMethod; +} diff --git a/packages/flutter_inappwebview_android/lib/src/find_interaction/find_interaction_controller.dart b/packages/flutter_inappwebview_android/lib/src/find_interaction/find_interaction_controller.dart new file mode 100644 index 0000000..943176a --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/find_interaction/find_interaction_controller.dart @@ -0,0 +1,125 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidFindInteractionController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformFindInteractionControllerCreationParams] for +/// more information. +@immutable +class AndroidFindInteractionControllerCreationParams + extends PlatformFindInteractionControllerCreationParams { + /// Creates a new [AndroidFindInteractionControllerCreationParams] instance. + const AndroidFindInteractionControllerCreationParams( + {super.onFindResultReceived}); + + /// Creates a [AndroidFindInteractionControllerCreationParams] instance based on [PlatformFindInteractionControllerCreationParams]. + factory AndroidFindInteractionControllerCreationParams.fromPlatformFindInteractionControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformFindInteractionControllerCreationParams params) { + return AndroidFindInteractionControllerCreationParams( + onFindResultReceived: params.onFindResultReceived); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController} +class AndroidFindInteractionController extends PlatformFindInteractionController + with ChannelController { + /// Constructs a [AndroidFindInteractionController]. + AndroidFindInteractionController( + PlatformFindInteractionControllerCreationParams params) + : super.implementation( + params is AndroidFindInteractionControllerCreationParams + ? params + : AndroidFindInteractionControllerCreationParams + .fromPlatformFindInteractionControllerCreationParams(params), + ); + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + debugLoggingSettings: + PlatformFindInteractionController.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onFindResultReceived": + if (onFindResultReceived != null) { + int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; + int numberOfMatches = call.arguments["numberOfMatches"]; + bool isDoneCounting = call.arguments["isDoneCounting"]; + onFindResultReceived!( + this, activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.findAll} + Future findAll({String? find}) async { + Map args = {}; + args.putIfAbsent('find', () => find); + await channel?.invokeMethod('findAll', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.findNext} + Future findNext({bool forward = true}) async { + Map args = {}; + args.putIfAbsent('forward', () => forward); + await channel?.invokeMethod('findNext', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.clearMatches} + Future clearMatches() async { + Map args = {}; + await channel?.invokeMethod('clearMatches', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.setSearchText} + Future setSearchText(String? searchText) async { + Map args = {}; + args.putIfAbsent('searchText', () => searchText); + await channel?.invokeMethod('setSearchText', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.getSearchText} + Future getSearchText() async { + Map args = {}; + return await channel?.invokeMethod('getSearchText', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.getActiveFindSession} + Future getActiveFindSession() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('getActiveFindSession', args)) + ?.cast(); + return FindSession.fromMap(result); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.dispose} + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(removeMethodCallHandler: !isKeepAlive); + } +} + +extension InternalFindInteractionController + on AndroidFindInteractionController { + void init(dynamic id) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_find_interaction_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } +} diff --git a/packages/flutter_inappwebview_android/lib/src/find_interaction/main.dart b/packages/flutter_inappwebview_android/lib/src/find_interaction/main.dart new file mode 100644 index 0000000..a7adaac --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/find_interaction/main.dart @@ -0,0 +1,2 @@ +export 'find_interaction_controller.dart' + hide InternalFindInteractionController; diff --git a/packages/flutter_inappwebview_android/lib/src/http_auth_credentials_database.dart b/packages/flutter_inappwebview_android/lib/src/http_auth_credentials_database.dart new file mode 100644 index 0000000..06ec37a --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/http_auth_credentials_database.dart @@ -0,0 +1,155 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidHttpAuthCredentialDatabase]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformHttpAuthCredentialDatabaseCreationParams] for +/// more information. +@immutable +class AndroidHttpAuthCredentialDatabaseCreationParams + extends PlatformHttpAuthCredentialDatabaseCreationParams { + /// Creates a new [AndroidHttpAuthCredentialDatabaseCreationParams] instance. + const AndroidHttpAuthCredentialDatabaseCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformHttpAuthCredentialDatabaseCreationParams params, + ) : super(); + + /// Creates a [AndroidHttpAuthCredentialDatabaseCreationParams] instance based on [PlatformHttpAuthCredentialDatabaseCreationParams]. + factory AndroidHttpAuthCredentialDatabaseCreationParams.fromPlatformHttpAuthCredentialDatabaseCreationParams( + PlatformHttpAuthCredentialDatabaseCreationParams params) { + return AndroidHttpAuthCredentialDatabaseCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformHttpAuthCredentialDatabase} +class AndroidHttpAuthCredentialDatabase + extends PlatformHttpAuthCredentialDatabase with ChannelController { + /// Creates a new [AndroidHttpAuthCredentialDatabase]. + AndroidHttpAuthCredentialDatabase( + PlatformHttpAuthCredentialDatabaseCreationParams params) + : super.implementation( + params is AndroidHttpAuthCredentialDatabaseCreationParams + ? params + : AndroidHttpAuthCredentialDatabaseCreationParams + .fromPlatformHttpAuthCredentialDatabaseCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_credential_database'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidHttpAuthCredentialDatabase? _instance; + + ///Gets the database shared instance. + static AndroidHttpAuthCredentialDatabase instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidHttpAuthCredentialDatabase _init() { + _instance = AndroidHttpAuthCredentialDatabase( + AndroidHttpAuthCredentialDatabaseCreationParams( + const PlatformHttpAuthCredentialDatabaseCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future> + getAllAuthCredentials() async { + Map args = {}; + List allCredentials = + await channel?.invokeMethod('getAllAuthCredentials', args) ?? []; + + List result = []; + + for (Map map in allCredentials) { + var element = URLProtectionSpaceHttpAuthCredentials.fromMap( + map.cast()); + if (element != null) { + result.add(element); + } + } + return result; + } + + @override + Future> getHttpAuthCredentials( + {required URLProtectionSpace protectionSpace}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + List credentialList = + await channel?.invokeMethod('getHttpAuthCredentials', args) ?? []; + List credentials = []; + for (Map map in credentialList) { + var credential = URLCredential.fromMap(map.cast()); + if (credential != null) { + credentials.add(credential); + } + } + return credentials; + } + + @override + Future setHttpAuthCredential( + {required URLProtectionSpace protectionSpace, + required URLCredential credential}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + args.putIfAbsent("username", () => credential.username); + args.putIfAbsent("password", () => credential.password); + await channel?.invokeMethod('setHttpAuthCredential', args); + } + + @override + Future removeHttpAuthCredential( + {required URLProtectionSpace protectionSpace, + required URLCredential credential}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + args.putIfAbsent("username", () => credential.username); + args.putIfAbsent("password", () => credential.password); + await channel?.invokeMethod('removeHttpAuthCredential', args); + } + + @override + Future removeHttpAuthCredentials( + {required URLProtectionSpace protectionSpace}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + await channel?.invokeMethod('removeHttpAuthCredentials', args); + } + + @override + Future clearAllAuthCredentials() async { + Map args = {}; + await channel?.invokeMethod('clearAllAuthCredentials', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalHttpAuthCredentialDatabase + on AndroidHttpAuthCredentialDatabase { + get handleMethod => _handleMethod; +} diff --git a/packages/flutter_inappwebview_android/lib/src/in_app_browser/in_app_browser.dart b/packages/flutter_inappwebview_android/lib/src/in_app_browser/in_app_browser.dart new file mode 100644 index 0000000..1c317c6 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/in_app_browser/in_app_browser.dart @@ -0,0 +1,375 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../find_interaction/find_interaction_controller.dart'; +import '../in_app_webview/in_app_webview_controller.dart'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; + +/// Object specifying creation parameters for creating a [AndroidInAppBrowser]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformInAppBrowserCreationParams] for +/// more information. +class AndroidInAppBrowserCreationParams + extends PlatformInAppBrowserCreationParams { + /// Creates a new [AndroidInAppBrowserCreationParams] instance. + AndroidInAppBrowserCreationParams( + {super.contextMenu, + this.pullToRefreshController, + this.findInteractionController, + super.initialUserScripts, + super.windowId}); + + /// Creates a [AndroidInAppBrowserCreationParams] instance based on [PlatformInAppBrowserCreationParams]. + factory AndroidInAppBrowserCreationParams.fromPlatformInAppBrowserCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformInAppBrowserCreationParams params) { + return AndroidInAppBrowserCreationParams( + contextMenu: params.contextMenu, + pullToRefreshController: + params.pullToRefreshController as AndroidPullToRefreshController?, + findInteractionController: params.findInteractionController + as AndroidFindInteractionController?, + initialUserScripts: params.initialUserScripts, + windowId: params.windowId); + } + + @override + final AndroidFindInteractionController? findInteractionController; + + @override + final AndroidPullToRefreshController? pullToRefreshController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformInAppBrowser} +class AndroidInAppBrowser extends PlatformInAppBrowser with ChannelController { + @override + final String id = IdGenerator.generate(); + + /// Constructs a [AndroidInAppBrowser]. + AndroidInAppBrowser(PlatformInAppBrowserCreationParams params) + : super.implementation( + params is AndroidInAppBrowserCreationParams + ? params + : AndroidInAppBrowserCreationParams + .fromPlatformInAppBrowserCreationParams(params), + ) { + _contextMenu = params.contextMenu; + } + + static final AndroidInAppBrowser _staticValue = + AndroidInAppBrowser(AndroidInAppBrowserCreationParams()); + + /// Provide static access. + factory AndroidInAppBrowser.static() { + return _staticValue; + } + + AndroidInAppBrowserCreationParams get _androidParams => + params as AndroidInAppBrowserCreationParams; + + static const MethodChannel _staticChannel = + const MethodChannel('com.pichillilorenzo/flutter_inappbrowser'); + + ContextMenu? _contextMenu; + + @override + ContextMenu? get contextMenu => _contextMenu; + + Map _menuItems = HashMap(); + bool _isOpened = false; + AndroidInAppWebViewController? _webViewController; + + @override + AndroidInAppWebViewController? get webViewController { + return _isOpened ? _webViewController : null; + } + + _init() { + channel = MethodChannel('com.pichillilorenzo/flutter_inappbrowser_$id'); + handler = _handleMethod; + initMethodCallHandler(); + + _webViewController = AndroidInAppWebViewController.fromInAppBrowser( + AndroidInAppWebViewControllerCreationParams(id: id), + channel!, + this, + this.initialUserScripts); + _androidParams.pullToRefreshController?.init(id); + _androidParams.findInteractionController?.init(id); + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + id: id, + debugLoggingSettings: PlatformInAppBrowser.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onBrowserCreated": + _debugLog(call.method, call.arguments); + eventHandler?.onBrowserCreated(); + break; + case "onMenuItemClicked": + _debugLog(call.method, call.arguments); + int id = call.arguments["id"].toInt(); + if (this._menuItems[id] != null) { + if (this._menuItems[id]?.onClick != null) { + this._menuItems[id]?.onClick!(); + } + } + break; + case "onExit": + _debugLog(call.method, call.arguments); + _isOpened = false; + final onExit = eventHandler?.onExit; + dispose(); + onExit?.call(); + break; + default: + return _webViewController?.handleMethod(call); + } + } + + Map _prepareOpenRequest( + {@Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) { + assert(!_isOpened, 'The browser is already opened.'); + _isOpened = true; + _init(); + + var initialSettings = settings?.toMap() ?? + options?.toMap() ?? + InAppBrowserClassSettings().toMap(); + + Map pullToRefreshSettings = + pullToRefreshController?.settings.toMap() ?? + pullToRefreshController?.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + List> menuItemList = []; + _menuItems.forEach((key, value) { + menuItemList.add(value.toMap()); + }); + + Map args = {}; + args.putIfAbsent('id', () => id); + args.putIfAbsent('settings', () => initialSettings); + args.putIfAbsent('contextMenu', () => contextMenu?.toMap() ?? {}); + args.putIfAbsent('windowId', () => windowId); + args.putIfAbsent('initialUserScripts', + () => initialUserScripts?.map((e) => e.toMap()).toList() ?? []); + args.putIfAbsent('pullToRefreshSettings', () => pullToRefreshSettings); + args.putIfAbsent('menuItems', () => menuItemList); + return args; + } + + @override + Future openUrlRequest( + {required URLRequest urlRequest, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + assert(urlRequest.url != null && urlRequest.url.toString().isNotEmpty); + + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openFile( + {required String assetFilePath, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + assert(assetFilePath.isNotEmpty); + + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('assetFilePath', () => assetFilePath); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openData( + {required String data, + String mimeType = "text/html", + String encoding = "utf8", + WebUri? baseUrl, + @Deprecated("Use historyUrl instead") Uri? androidHistoryUrl, + WebUri? historyUrl, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('data', () => data); + args.putIfAbsent('mimeType', () => mimeType); + args.putIfAbsent('encoding', () => encoding); + args.putIfAbsent('baseUrl', () => baseUrl?.toString() ?? "about:blank"); + args.putIfAbsent('historyUrl', + () => (historyUrl ?? androidHistoryUrl)?.toString() ?? "about:blank"); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openWithSystemBrowser({required WebUri url}) async { + assert(url.toString().isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + return await _staticChannel.invokeMethod('openWithSystemBrowser', args); + } + + @override + void addMenuItem(InAppBrowserMenuItem menuItem) { + _menuItems[menuItem.id] = menuItem; + } + + @override + void addMenuItems(List menuItems) { + menuItems.forEach((menuItem) { + _menuItems[menuItem.id] = menuItem; + }); + } + + @override + bool removeMenuItem(InAppBrowserMenuItem menuItem) { + return _menuItems.remove(menuItem.id) != null; + } + + @override + void removeMenuItems(List menuItems) { + for (final menuItem in menuItems) { + removeMenuItem(menuItem); + } + } + + @override + void removeAllMenuItem() { + _menuItems.clear(); + } + + @override + bool hasMenuItem(InAppBrowserMenuItem menuItem) { + return _menuItems.containsKey(menuItem.id); + } + + @override + Future show() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('show', args); + } + + @override + Future hide() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('hide', args); + } + + @override + Future close() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('close', args); + } + + @override + Future isHidden() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + return await channel?.invokeMethod('isHidden', args) ?? false; + } + + @override + @Deprecated('Use setSettings instead') + Future setOptions({required InAppBrowserClassOptions options}) async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + args.putIfAbsent('settings', () => options.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + @Deprecated('Use getSettings instead') + Future getOptions() async { + assert(_isOpened, 'The browser is not opened.'); + Map args = {}; + + Map? options = + await channel?.invokeMethod('getSettings', args); + if (options != null) { + options = options.cast(); + return InAppBrowserClassOptions.fromMap(options as Map); + } + + return null; + } + + @override + Future setSettings( + {required InAppBrowserClassSettings settings}) async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + args.putIfAbsent('settings', () => settings.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + Future getSettings() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + + Map? settings = + await channel?.invokeMethod('getSettings', args); + if (settings != null) { + settings = settings.cast(); + return InAppBrowserClassSettings.fromMap( + settings as Map); + } + + return null; + } + + @override + bool isOpened() { + return this._isOpened; + } + + @override + @mustCallSuper + void dispose() { + super.dispose(); + disposeChannel(); + _webViewController?.dispose(); + _webViewController = null; + pullToRefreshController?.dispose(); + findInteractionController?.dispose(); + } +} + +extension InternalInAppBrowser on AndroidInAppBrowser { + void setContextMenu(ContextMenu? contextMenu) { + _contextMenu = contextMenu; + } +} diff --git a/packages/flutter_inappwebview_android/lib/src/in_app_browser/main.dart b/packages/flutter_inappwebview_android/lib/src/in_app_browser/main.dart new file mode 100644 index 0000000..e11eb8b --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/in_app_browser/main.dart @@ -0,0 +1 @@ +export 'in_app_browser.dart' hide InternalInAppBrowser; diff --git a/packages/flutter_inappwebview_android/lib/src/in_app_webview/_static_channel.dart b/packages/flutter_inappwebview_android/lib/src/in_app_webview/_static_channel.dart new file mode 100644 index 0000000..beb7de7 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/in_app_webview/_static_channel.dart @@ -0,0 +1,4 @@ +import 'package:flutter/services.dart'; + +const IN_APP_WEBVIEW_STATIC_CHANNEL = + MethodChannel('com.pichillilorenzo/flutter_inappwebview_manager'); diff --git a/packages/flutter_inappwebview_android/lib/src/in_app_webview/headless_in_app_webview.dart b/packages/flutter_inappwebview_android/lib/src/in_app_webview/headless_in_app_webview.dart new file mode 100644 index 0000000..c53a36d --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/in_app_webview/headless_in_app_webview.dart @@ -0,0 +1,440 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import '../find_interaction/find_interaction_controller.dart'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; +import 'in_app_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [AndroidHeadlessInAppWebView]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformHeadlessInAppWebViewCreationParams] for +/// more information. +@immutable +class AndroidHeadlessInAppWebViewCreationParams + extends PlatformHeadlessInAppWebViewCreationParams { + /// Creates a new [AndroidHeadlessInAppWebViewCreationParams] instance. + AndroidHeadlessInAppWebViewCreationParams( + {super.controllerFromPlatform, + super.initialSize, + super.windowId, + super.onWebViewCreated, + super.onLoadStart, + super.onLoadStop, + @Deprecated('Use onReceivedError instead') super.onLoadError, + super.onReceivedError, + @Deprecated("Use onReceivedHttpError instead") super.onLoadHttpError, + super.onReceivedHttpError, + super.onProgressChanged, + super.onConsoleMessage, + super.shouldOverrideUrlLoading, + super.onLoadResource, + super.onScrollChanged, + @Deprecated('Use onDownloadStartRequest instead') super.onDownloadStart, + super.onDownloadStartRequest, + @Deprecated('Use onLoadResourceWithCustomScheme instead') + super.onLoadResourceCustomScheme, + super.onLoadResourceWithCustomScheme, + super.onCreateWindow, + super.onCloseWindow, + super.onJsAlert, + super.onJsConfirm, + super.onJsPrompt, + super.onReceivedHttpAuthRequest, + super.onReceivedServerTrustAuthRequest, + super.onReceivedClientCertRequest, + @Deprecated('Use FindInteractionController.onFindResultReceived instead') + super.onFindResultReceived, + super.shouldInterceptAjaxRequest, + super.onAjaxReadyStateChange, + super.onAjaxProgress, + super.shouldInterceptFetchRequest, + super.onUpdateVisitedHistory, + @Deprecated("Use onPrintRequest instead") super.onPrint, + super.onPrintRequest, + super.onLongPressHitTestResult, + super.onEnterFullscreen, + super.onExitFullscreen, + super.onPageCommitVisible, + super.onTitleChanged, + super.onWindowFocus, + super.onWindowBlur, + super.onOverScrolled, + super.onZoomScaleChanged, + @Deprecated('Use onSafeBrowsingHit instead') + super.androidOnSafeBrowsingHit, + super.onSafeBrowsingHit, + @Deprecated('Use onPermissionRequest instead') + super.androidOnPermissionRequest, + super.onPermissionRequest, + @Deprecated('Use onGeolocationPermissionsShowPrompt instead') + super.androidOnGeolocationPermissionsShowPrompt, + super.onGeolocationPermissionsShowPrompt, + @Deprecated('Use onGeolocationPermissionsHidePrompt instead') + super.androidOnGeolocationPermissionsHidePrompt, + super.onGeolocationPermissionsHidePrompt, + @Deprecated('Use shouldInterceptRequest instead') + super.androidShouldInterceptRequest, + super.shouldInterceptRequest, + @Deprecated('Use onRenderProcessGone instead') + super.androidOnRenderProcessGone, + super.onRenderProcessGone, + @Deprecated('Use onRenderProcessResponsive instead') + super.androidOnRenderProcessResponsive, + super.onRenderProcessResponsive, + @Deprecated('Use onRenderProcessUnresponsive instead') + super.androidOnRenderProcessUnresponsive, + super.onRenderProcessUnresponsive, + @Deprecated('Use onFormResubmission instead') + super.androidOnFormResubmission, + super.onFormResubmission, + @Deprecated('Use onZoomScaleChanged instead') super.androidOnScaleChanged, + @Deprecated('Use onReceivedIcon instead') super.androidOnReceivedIcon, + super.onReceivedIcon, + @Deprecated('Use onReceivedTouchIconUrl instead') + super.androidOnReceivedTouchIconUrl, + super.onReceivedTouchIconUrl, + @Deprecated('Use onJsBeforeUnload instead') super.androidOnJsBeforeUnload, + super.onJsBeforeUnload, + @Deprecated('Use onReceivedLoginRequest instead') + super.androidOnReceivedLoginRequest, + super.onReceivedLoginRequest, + super.onPermissionRequestCanceled, + super.onRequestFocus, + @Deprecated('Use onWebContentProcessDidTerminate instead') + super.iosOnWebContentProcessDidTerminate, + super.onWebContentProcessDidTerminate, + @Deprecated( + 'Use onDidReceiveServerRedirectForProvisionalNavigation instead') + super.iosOnDidReceiveServerRedirectForProvisionalNavigation, + super.onDidReceiveServerRedirectForProvisionalNavigation, + @Deprecated('Use onNavigationResponse instead') + super.iosOnNavigationResponse, + super.onNavigationResponse, + @Deprecated('Use shouldAllowDeprecatedTLS instead') + super.iosShouldAllowDeprecatedTLS, + super.shouldAllowDeprecatedTLS, + super.onCameraCaptureStateChanged, + super.onMicrophoneCaptureStateChanged, + super.onContentSizeChanged, + super.initialUrlRequest, + super.initialFile, + super.initialData, + @Deprecated('Use initialSettings instead') super.initialOptions, + super.initialSettings, + super.contextMenu, + super.initialUserScripts, + this.pullToRefreshController, + this.findInteractionController}); + + /// Creates a [AndroidHeadlessInAppWebViewCreationParams] instance based on [PlatformHeadlessInAppWebViewCreationParams]. + AndroidHeadlessInAppWebViewCreationParams.fromPlatformHeadlessInAppWebViewCreationParams( + PlatformHeadlessInAppWebViewCreationParams params) + : this( + controllerFromPlatform: params.controllerFromPlatform, + initialSize: params.initialSize, + windowId: params.windowId, + onWebViewCreated: params.onWebViewCreated, + onLoadStart: params.onLoadStart, + onLoadStop: params.onLoadStop, + onLoadError: params.onLoadError, + onReceivedError: params.onReceivedError, + onLoadHttpError: params.onLoadHttpError, + onReceivedHttpError: params.onReceivedHttpError, + onProgressChanged: params.onProgressChanged, + onConsoleMessage: params.onConsoleMessage, + shouldOverrideUrlLoading: params.shouldOverrideUrlLoading, + onLoadResource: params.onLoadResource, + onScrollChanged: params.onScrollChanged, + onDownloadStart: params.onDownloadStart, + onDownloadStartRequest: params.onDownloadStartRequest, + onLoadResourceCustomScheme: params.onLoadResourceCustomScheme, + onLoadResourceWithCustomScheme: + params.onLoadResourceWithCustomScheme, + onCreateWindow: params.onCreateWindow, + onCloseWindow: params.onCloseWindow, + onJsAlert: params.onJsAlert, + onJsConfirm: params.onJsConfirm, + onJsPrompt: params.onJsPrompt, + onReceivedHttpAuthRequest: params.onReceivedHttpAuthRequest, + onReceivedServerTrustAuthRequest: + params.onReceivedServerTrustAuthRequest, + onReceivedClientCertRequest: params.onReceivedClientCertRequest, + onFindResultReceived: params.onFindResultReceived, + shouldInterceptAjaxRequest: params.shouldInterceptAjaxRequest, + onAjaxReadyStateChange: params.onAjaxReadyStateChange, + onAjaxProgress: params.onAjaxProgress, + shouldInterceptFetchRequest: params.shouldInterceptFetchRequest, + onUpdateVisitedHistory: params.onUpdateVisitedHistory, + onPrint: params.onPrint, + onPrintRequest: params.onPrintRequest, + onLongPressHitTestResult: params.onLongPressHitTestResult, + onEnterFullscreen: params.onEnterFullscreen, + onExitFullscreen: params.onExitFullscreen, + onPageCommitVisible: params.onPageCommitVisible, + onTitleChanged: params.onTitleChanged, + onWindowFocus: params.onWindowFocus, + onWindowBlur: params.onWindowBlur, + onOverScrolled: params.onOverScrolled, + onZoomScaleChanged: params.onZoomScaleChanged, + androidOnSafeBrowsingHit: params.androidOnSafeBrowsingHit, + onSafeBrowsingHit: params.onSafeBrowsingHit, + androidOnPermissionRequest: params.androidOnPermissionRequest, + onPermissionRequest: params.onPermissionRequest, + androidOnGeolocationPermissionsShowPrompt: + params.androidOnGeolocationPermissionsShowPrompt, + onGeolocationPermissionsShowPrompt: + params.onGeolocationPermissionsShowPrompt, + androidOnGeolocationPermissionsHidePrompt: + params.androidOnGeolocationPermissionsHidePrompt, + onGeolocationPermissionsHidePrompt: + params.onGeolocationPermissionsHidePrompt, + androidShouldInterceptRequest: params.androidShouldInterceptRequest, + shouldInterceptRequest: params.shouldInterceptRequest, + androidOnRenderProcessGone: params.androidOnRenderProcessGone, + onRenderProcessGone: params.onRenderProcessGone, + androidOnRenderProcessResponsive: + params.androidOnRenderProcessResponsive, + onRenderProcessResponsive: params.onRenderProcessResponsive, + androidOnRenderProcessUnresponsive: + params.androidOnRenderProcessUnresponsive, + onRenderProcessUnresponsive: params.onRenderProcessUnresponsive, + androidOnFormResubmission: params.androidOnFormResubmission, + onFormResubmission: params.onFormResubmission, + androidOnScaleChanged: params.androidOnScaleChanged, + androidOnReceivedIcon: params.androidOnReceivedIcon, + onReceivedIcon: params.onReceivedIcon, + androidOnReceivedTouchIconUrl: params.androidOnReceivedTouchIconUrl, + onReceivedTouchIconUrl: params.onReceivedTouchIconUrl, + androidOnJsBeforeUnload: params.androidOnJsBeforeUnload, + onJsBeforeUnload: params.onJsBeforeUnload, + androidOnReceivedLoginRequest: params.androidOnReceivedLoginRequest, + onReceivedLoginRequest: params.onReceivedLoginRequest, + onPermissionRequestCanceled: params.onPermissionRequestCanceled, + onRequestFocus: params.onRequestFocus, + iosOnWebContentProcessDidTerminate: + params.iosOnWebContentProcessDidTerminate, + onWebContentProcessDidTerminate: + params.onWebContentProcessDidTerminate, + iosOnDidReceiveServerRedirectForProvisionalNavigation: + params.iosOnDidReceiveServerRedirectForProvisionalNavigation, + onDidReceiveServerRedirectForProvisionalNavigation: + params.onDidReceiveServerRedirectForProvisionalNavigation, + iosOnNavigationResponse: params.iosOnNavigationResponse, + onNavigationResponse: params.onNavigationResponse, + iosShouldAllowDeprecatedTLS: params.iosShouldAllowDeprecatedTLS, + shouldAllowDeprecatedTLS: params.shouldAllowDeprecatedTLS, + onCameraCaptureStateChanged: params.onCameraCaptureStateChanged, + onMicrophoneCaptureStateChanged: + params.onMicrophoneCaptureStateChanged, + onContentSizeChanged: params.onContentSizeChanged, + initialUrlRequest: params.initialUrlRequest, + initialFile: params.initialFile, + initialData: params.initialData, + initialOptions: params.initialOptions, + initialSettings: params.initialSettings, + contextMenu: params.contextMenu, + initialUserScripts: params.initialUserScripts, + pullToRefreshController: params.pullToRefreshController + as AndroidPullToRefreshController?, + findInteractionController: params.findInteractionController + as AndroidFindInteractionController?); + + @override + final AndroidFindInteractionController? findInteractionController; + + @override + final AndroidPullToRefreshController? pullToRefreshController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformHeadlessInAppWebView} +class AndroidHeadlessInAppWebView extends PlatformHeadlessInAppWebView + with ChannelController { + @override + late final String id; + + bool _started = false; + bool _running = false; + + static const MethodChannel _sharedChannel = + const MethodChannel('com.pichillilorenzo/flutter_headless_inappwebview'); + + AndroidInAppWebViewController? _webViewController; + + /// Constructs a [AndroidHeadlessInAppWebView]. + AndroidHeadlessInAppWebView(PlatformHeadlessInAppWebViewCreationParams params) + : super.implementation( + params is AndroidHeadlessInAppWebViewCreationParams + ? params + : AndroidHeadlessInAppWebViewCreationParams + .fromPlatformHeadlessInAppWebViewCreationParams(params), + ) { + id = IdGenerator.generate(); + } + + @override + AndroidInAppWebViewController? get webViewController => _webViewController; + + dynamic _controllerFromPlatform; + + AndroidHeadlessInAppWebViewCreationParams get _androidParams => + params as AndroidHeadlessInAppWebViewCreationParams; + + _init() { + _webViewController = AndroidInAppWebViewController( + AndroidInAppWebViewControllerCreationParams( + id: id, webviewParams: params), + ); + _controllerFromPlatform = + params.controllerFromPlatform?.call(_webViewController!) ?? + _webViewController!; + _androidParams.pullToRefreshController?.init(id); + _androidParams.findInteractionController?.init(id); + channel = + MethodChannel('com.pichillilorenzo/flutter_headless_inappwebview_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onWebViewCreated": + if (params.onWebViewCreated != null && _webViewController != null) { + params.onWebViewCreated!(_controllerFromPlatform); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + Future run() async { + if (_started) { + return; + } + _started = true; + _init(); + + final initialSettings = params.initialSettings ?? InAppWebViewSettings(); + _inferInitialSettings(initialSettings); + + Map settingsMap = + (params.initialSettings != null ? initialSettings.toMap() : null) ?? + params.initialOptions?.toMap() ?? + initialSettings.toMap(); + + Map pullToRefreshSettings = + _androidParams.pullToRefreshController?.params.settings.toMap() ?? + _androidParams.pullToRefreshController?.params.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + Map args = {}; + args.putIfAbsent('id', () => id); + args.putIfAbsent( + 'params', + () => { + 'initialUrlRequest': params.initialUrlRequest?.toMap(), + 'initialFile': params.initialFile, + 'initialData': params.initialData?.toMap(), + 'initialSettings': settingsMap, + 'contextMenu': params.contextMenu?.toMap() ?? {}, + 'windowId': params.windowId, + 'initialUserScripts': + params.initialUserScripts?.map((e) => e.toMap()).toList() ?? + [], + 'pullToRefreshSettings': pullToRefreshSettings, + 'initialSize': params.initialSize.toMap() + }); + await _sharedChannel.invokeMethod('run', args); + _running = true; + } + + void _inferInitialSettings(InAppWebViewSettings settings) { + if (params.shouldOverrideUrlLoading != null && + settings.useShouldOverrideUrlLoading == null) { + settings.useShouldOverrideUrlLoading = true; + } + if (params.onLoadResource != null && settings.useOnLoadResource == null) { + settings.useOnLoadResource = true; + } + if (params.onDownloadStartRequest != null && + settings.useOnDownloadStart == null) { + settings.useOnDownloadStart = true; + } + if (params.shouldInterceptAjaxRequest != null && + settings.useShouldInterceptAjaxRequest == null) { + settings.useShouldInterceptAjaxRequest = true; + } + if (params.shouldInterceptFetchRequest != null && + settings.useShouldInterceptFetchRequest == null) { + settings.useShouldInterceptFetchRequest = true; + } + if (params.shouldInterceptRequest != null && + settings.useShouldInterceptRequest == null) { + settings.useShouldInterceptRequest = true; + } + if (params.onRenderProcessGone != null && + settings.useOnRenderProcessGone == null) { + settings.useOnRenderProcessGone = true; + } + if (params.onNavigationResponse != null && + settings.useOnNavigationResponse == null) { + settings.useOnNavigationResponse = true; + } + } + + @override + bool isRunning() { + return _running; + } + + @override + Future setSize(Size size) async { + if (!_running) { + return; + } + + Map args = {}; + args.putIfAbsent('size', () => size.toMap()); + await channel?.invokeMethod('setSize', args); + } + + @override + Future getSize() async { + if (!_running) { + return null; + } + + Map args = {}; + Map sizeMap = + (await channel?.invokeMethod('getSize', args))?.cast(); + return MapSize.fromMap(sizeMap); + } + + @override + Future dispose() async { + if (!_running) { + return; + } + Map args = {}; + await channel?.invokeMethod('dispose', args); + disposeChannel(); + _started = false; + _running = false; + _webViewController?.dispose(); + _webViewController = null; + _controllerFromPlatform = null; + _androidParams.pullToRefreshController?.dispose(); + _androidParams.findInteractionController?.dispose(); + } +} + +extension InternalHeadlessInAppWebView on AndroidHeadlessInAppWebView { + Future internalDispose() async { + _started = false; + _running = false; + } +} diff --git a/packages/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview.dart b/packages/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview.dart new file mode 100644 index 0000000..16838e2 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview.dart @@ -0,0 +1,475 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import 'headless_in_app_webview.dart'; + +import '../find_interaction/find_interaction_controller.dart'; +import 'in_app_webview_controller.dart'; +import '../pull_to_refresh/main.dart'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; + +/// Object specifying creation parameters for creating a [PlatformInAppWebViewWidget]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +class AndroidInAppWebViewWidgetCreationParams + extends PlatformInAppWebViewWidgetCreationParams { + AndroidInAppWebViewWidgetCreationParams( + {super.controllerFromPlatform, + super.key, + super.layoutDirection, + super.gestureRecognizers, + super.headlessWebView, + super.keepAlive, + super.preventGestureDelay, + super.windowId, + super.onWebViewCreated, + super.onLoadStart, + super.onLoadStop, + @Deprecated('Use onReceivedError instead') super.onLoadError, + super.onReceivedError, + @Deprecated("Use onReceivedHttpError instead") super.onLoadHttpError, + super.onReceivedHttpError, + super.onProgressChanged, + super.onConsoleMessage, + super.shouldOverrideUrlLoading, + super.onLoadResource, + super.onScrollChanged, + @Deprecated('Use onDownloadStartRequest instead') super.onDownloadStart, + super.onDownloadStartRequest, + @Deprecated('Use onLoadResourceWithCustomScheme instead') + super.onLoadResourceCustomScheme, + super.onLoadResourceWithCustomScheme, + super.onCreateWindow, + super.onCloseWindow, + super.onJsAlert, + super.onJsConfirm, + super.onJsPrompt, + super.onReceivedHttpAuthRequest, + super.onReceivedServerTrustAuthRequest, + super.onReceivedClientCertRequest, + @Deprecated('Use FindInteractionController.onFindResultReceived instead') + super.onFindResultReceived, + super.shouldInterceptAjaxRequest, + super.onAjaxReadyStateChange, + super.onAjaxProgress, + super.shouldInterceptFetchRequest, + super.onUpdateVisitedHistory, + @Deprecated("Use onPrintRequest instead") super.onPrint, + super.onPrintRequest, + super.onLongPressHitTestResult, + super.onEnterFullscreen, + super.onExitFullscreen, + super.onPageCommitVisible, + super.onTitleChanged, + super.onWindowFocus, + super.onWindowBlur, + super.onOverScrolled, + super.onZoomScaleChanged, + @Deprecated('Use onSafeBrowsingHit instead') + super.androidOnSafeBrowsingHit, + super.onSafeBrowsingHit, + @Deprecated('Use onPermissionRequest instead') + super.androidOnPermissionRequest, + super.onPermissionRequest, + @Deprecated('Use onGeolocationPermissionsShowPrompt instead') + super.androidOnGeolocationPermissionsShowPrompt, + super.onGeolocationPermissionsShowPrompt, + @Deprecated('Use onGeolocationPermissionsHidePrompt instead') + super.androidOnGeolocationPermissionsHidePrompt, + super.onGeolocationPermissionsHidePrompt, + @Deprecated('Use shouldInterceptRequest instead') + super.androidShouldInterceptRequest, + super.shouldInterceptRequest, + @Deprecated('Use onRenderProcessGone instead') + super.androidOnRenderProcessGone, + super.onRenderProcessGone, + @Deprecated('Use onRenderProcessResponsive instead') + super.androidOnRenderProcessResponsive, + super.onRenderProcessResponsive, + @Deprecated('Use onRenderProcessUnresponsive instead') + super.androidOnRenderProcessUnresponsive, + super.onRenderProcessUnresponsive, + @Deprecated('Use onFormResubmission instead') + super.androidOnFormResubmission, + super.onFormResubmission, + @Deprecated('Use onZoomScaleChanged instead') super.androidOnScaleChanged, + @Deprecated('Use onReceivedIcon instead') super.androidOnReceivedIcon, + super.onReceivedIcon, + @Deprecated('Use onReceivedTouchIconUrl instead') + super.androidOnReceivedTouchIconUrl, + super.onReceivedTouchIconUrl, + @Deprecated('Use onJsBeforeUnload instead') super.androidOnJsBeforeUnload, + super.onJsBeforeUnload, + @Deprecated('Use onReceivedLoginRequest instead') + super.androidOnReceivedLoginRequest, + super.onReceivedLoginRequest, + super.onPermissionRequestCanceled, + super.onRequestFocus, + @Deprecated('Use onWebContentProcessDidTerminate instead') + super.iosOnWebContentProcessDidTerminate, + super.onWebContentProcessDidTerminate, + @Deprecated( + 'Use onDidReceiveServerRedirectForProvisionalNavigation instead') + super.iosOnDidReceiveServerRedirectForProvisionalNavigation, + super.onDidReceiveServerRedirectForProvisionalNavigation, + @Deprecated('Use onNavigationResponse instead') + super.iosOnNavigationResponse, + super.onNavigationResponse, + @Deprecated('Use shouldAllowDeprecatedTLS instead') + super.iosShouldAllowDeprecatedTLS, + super.shouldAllowDeprecatedTLS, + super.onCameraCaptureStateChanged, + super.onMicrophoneCaptureStateChanged, + super.onContentSizeChanged, + super.initialUrlRequest, + super.initialFile, + super.initialData, + @Deprecated('Use initialSettings instead') super.initialOptions, + super.initialSettings, + super.contextMenu, + super.initialUserScripts, + this.pullToRefreshController, + this.findInteractionController}); + + /// Constructs a [AndroidInAppWebViewWidgetCreationParams] using a + /// [PlatformInAppWebViewWidgetCreationParams]. + AndroidInAppWebViewWidgetCreationParams.fromPlatformInAppWebViewWidgetCreationParams( + PlatformInAppWebViewWidgetCreationParams params) + : this( + controllerFromPlatform: params.controllerFromPlatform, + key: params.key, + layoutDirection: params.layoutDirection, + gestureRecognizers: params.gestureRecognizers, + headlessWebView: params.headlessWebView, + keepAlive: params.keepAlive, + preventGestureDelay: params.preventGestureDelay, + windowId: params.windowId, + onWebViewCreated: params.onWebViewCreated, + onLoadStart: params.onLoadStart, + onLoadStop: params.onLoadStop, + onLoadError: params.onLoadError, + onReceivedError: params.onReceivedError, + onLoadHttpError: params.onLoadHttpError, + onReceivedHttpError: params.onReceivedHttpError, + onProgressChanged: params.onProgressChanged, + onConsoleMessage: params.onConsoleMessage, + shouldOverrideUrlLoading: params.shouldOverrideUrlLoading, + onLoadResource: params.onLoadResource, + onScrollChanged: params.onScrollChanged, + onDownloadStart: params.onDownloadStart, + onDownloadStartRequest: params.onDownloadStartRequest, + onLoadResourceCustomScheme: params.onLoadResourceCustomScheme, + onLoadResourceWithCustomScheme: + params.onLoadResourceWithCustomScheme, + onCreateWindow: params.onCreateWindow, + onCloseWindow: params.onCloseWindow, + onJsAlert: params.onJsAlert, + onJsConfirm: params.onJsConfirm, + onJsPrompt: params.onJsPrompt, + onReceivedHttpAuthRequest: params.onReceivedHttpAuthRequest, + onReceivedServerTrustAuthRequest: + params.onReceivedServerTrustAuthRequest, + onReceivedClientCertRequest: params.onReceivedClientCertRequest, + onFindResultReceived: params.onFindResultReceived, + shouldInterceptAjaxRequest: params.shouldInterceptAjaxRequest, + onAjaxReadyStateChange: params.onAjaxReadyStateChange, + onAjaxProgress: params.onAjaxProgress, + shouldInterceptFetchRequest: params.shouldInterceptFetchRequest, + onUpdateVisitedHistory: params.onUpdateVisitedHistory, + onPrint: params.onPrint, + onPrintRequest: params.onPrintRequest, + onLongPressHitTestResult: params.onLongPressHitTestResult, + onEnterFullscreen: params.onEnterFullscreen, + onExitFullscreen: params.onExitFullscreen, + onPageCommitVisible: params.onPageCommitVisible, + onTitleChanged: params.onTitleChanged, + onWindowFocus: params.onWindowFocus, + onWindowBlur: params.onWindowBlur, + onOverScrolled: params.onOverScrolled, + onZoomScaleChanged: params.onZoomScaleChanged, + androidOnSafeBrowsingHit: params.androidOnSafeBrowsingHit, + onSafeBrowsingHit: params.onSafeBrowsingHit, + androidOnPermissionRequest: params.androidOnPermissionRequest, + onPermissionRequest: params.onPermissionRequest, + androidOnGeolocationPermissionsShowPrompt: + params.androidOnGeolocationPermissionsShowPrompt, + onGeolocationPermissionsShowPrompt: + params.onGeolocationPermissionsShowPrompt, + androidOnGeolocationPermissionsHidePrompt: + params.androidOnGeolocationPermissionsHidePrompt, + onGeolocationPermissionsHidePrompt: + params.onGeolocationPermissionsHidePrompt, + androidShouldInterceptRequest: params.androidShouldInterceptRequest, + shouldInterceptRequest: params.shouldInterceptRequest, + androidOnRenderProcessGone: params.androidOnRenderProcessGone, + onRenderProcessGone: params.onRenderProcessGone, + androidOnRenderProcessResponsive: + params.androidOnRenderProcessResponsive, + onRenderProcessResponsive: params.onRenderProcessResponsive, + androidOnRenderProcessUnresponsive: + params.androidOnRenderProcessUnresponsive, + onRenderProcessUnresponsive: params.onRenderProcessUnresponsive, + androidOnFormResubmission: params.androidOnFormResubmission, + onFormResubmission: params.onFormResubmission, + androidOnScaleChanged: params.androidOnScaleChanged, + androidOnReceivedIcon: params.androidOnReceivedIcon, + onReceivedIcon: params.onReceivedIcon, + androidOnReceivedTouchIconUrl: params.androidOnReceivedTouchIconUrl, + onReceivedTouchIconUrl: params.onReceivedTouchIconUrl, + androidOnJsBeforeUnload: params.androidOnJsBeforeUnload, + onJsBeforeUnload: params.onJsBeforeUnload, + androidOnReceivedLoginRequest: params.androidOnReceivedLoginRequest, + onReceivedLoginRequest: params.onReceivedLoginRequest, + onPermissionRequestCanceled: params.onPermissionRequestCanceled, + onRequestFocus: params.onRequestFocus, + iosOnWebContentProcessDidTerminate: + params.iosOnWebContentProcessDidTerminate, + onWebContentProcessDidTerminate: + params.onWebContentProcessDidTerminate, + iosOnDidReceiveServerRedirectForProvisionalNavigation: + params.iosOnDidReceiveServerRedirectForProvisionalNavigation, + onDidReceiveServerRedirectForProvisionalNavigation: + params.onDidReceiveServerRedirectForProvisionalNavigation, + iosOnNavigationResponse: params.iosOnNavigationResponse, + onNavigationResponse: params.onNavigationResponse, + iosShouldAllowDeprecatedTLS: params.iosShouldAllowDeprecatedTLS, + shouldAllowDeprecatedTLS: params.shouldAllowDeprecatedTLS, + onCameraCaptureStateChanged: params.onCameraCaptureStateChanged, + onMicrophoneCaptureStateChanged: + params.onMicrophoneCaptureStateChanged, + onContentSizeChanged: params.onContentSizeChanged, + initialUrlRequest: params.initialUrlRequest, + initialFile: params.initialFile, + initialData: params.initialData, + initialOptions: params.initialOptions, + initialSettings: params.initialSettings, + contextMenu: params.contextMenu, + initialUserScripts: params.initialUserScripts, + pullToRefreshController: params.pullToRefreshController + as AndroidPullToRefreshController?, + findInteractionController: params.findInteractionController + as AndroidFindInteractionController?); + + @override + final AndroidFindInteractionController? findInteractionController; + + @override + final AndroidPullToRefreshController? pullToRefreshController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformInAppWebViewWidget} +class AndroidInAppWebViewWidget extends PlatformInAppWebViewWidget { + /// Constructs a [AndroidInAppWebViewWidget]. + /// + ///{@macro flutter_inappwebview_platform_interface.PlatformInAppWebViewWidget} + AndroidInAppWebViewWidget(PlatformInAppWebViewWidgetCreationParams params) + : super.implementation( + params is AndroidInAppWebViewWidgetCreationParams + ? params + : AndroidInAppWebViewWidgetCreationParams + .fromPlatformInAppWebViewWidgetCreationParams(params), + ); + + AndroidInAppWebViewWidgetCreationParams get _androidParams => + params as AndroidInAppWebViewWidgetCreationParams; + + AndroidInAppWebViewController? _controller; + + AndroidHeadlessInAppWebView? get _androidHeadlessInAppWebView => + params.headlessWebView as AndroidHeadlessInAppWebView?; + + @override + Widget build(BuildContext context) { + final initialSettings = params.initialSettings ?? InAppWebViewSettings(); + _inferInitialSettings(initialSettings); + + Map settingsMap = + (params.initialSettings != null ? initialSettings.toMap() : null) ?? + // ignore: deprecated_member_use_from_same_package + params.initialOptions?.toMap() ?? + initialSettings.toMap(); + + Map pullToRefreshSettings = + params.pullToRefreshController?.params.settings.toMap() ?? + // ignore: deprecated_member_use_from_same_package + params.pullToRefreshController?.params.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + if ((params.headlessWebView?.isRunning() ?? false) && + params.keepAlive != null) { + final headlessId = params.headlessWebView?.id; + if (headlessId != null) { + // force keep alive id to match headless webview id + params.keepAlive?.id = headlessId; + } + } + + var useHybridComposition = (params.initialSettings != null + ? initialSettings.useHybridComposition + : params.initialOptions?.android.useHybridComposition) ?? + true; + + return PlatformViewLink( + key: params.key, + viewType: 'com.pichillilorenzo/flutter_inappwebview', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: params.gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return _createAndroidViewController( + hybridComposition: useHybridComposition, + id: params.id, + viewType: 'com.pichillilorenzo/flutter_inappwebview', + layoutDirection: this.params.layoutDirection ?? + Directionality.maybeOf(context) ?? + TextDirection.rtl, + creationParams: { + 'initialUrlRequest': this.params.initialUrlRequest?.toMap(), + 'initialFile': this.params.initialFile, + 'initialData': this.params.initialData?.toMap(), + 'initialSettings': settingsMap, + 'contextMenu': this.params.contextMenu?.toMap() ?? {}, + 'windowId': this.params.windowId, + 'headlessWebViewId': + this.params.headlessWebView?.isRunning() ?? false + ? this.params.headlessWebView?.id + : null, + 'initialUserScripts': this + .params + .initialUserScripts + ?.map((e) => e.toMap()) + .toList() ?? + [], + 'pullToRefreshSettings': pullToRefreshSettings, + 'keepAliveId': this.params.keepAlive?.id + }, + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..addOnPlatformViewCreatedListener((id) => _onPlatformViewCreated(id)) + ..create(); + }, + ); + } + + AndroidViewController _createAndroidViewController({ + required bool hybridComposition, + required int id, + required String viewType, + required TextDirection layoutDirection, + required Map creationParams, + }) { + if (hybridComposition) { + return PlatformViewsService.initExpensiveAndroidView( + id: id, + viewType: viewType, + layoutDirection: layoutDirection, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } + return PlatformViewsService.initSurfaceAndroidView( + id: id, + viewType: viewType, + layoutDirection: layoutDirection, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } + + void _onPlatformViewCreated(int id) { + dynamic viewId = id; + if (params.headlessWebView?.isRunning() ?? false) { + viewId = params.headlessWebView?.id; + } + viewId = params.keepAlive?.id ?? viewId ?? id; + _androidHeadlessInAppWebView?.internalDispose(); + _controller = AndroidInAppWebViewController( + PlatformInAppWebViewControllerCreationParams( + id: viewId, webviewParams: params)); + _androidParams.pullToRefreshController?.init(viewId); + _androidParams.findInteractionController?.init(viewId); + debugLog( + className: runtimeType.toString(), + id: viewId?.toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: "onWebViewCreated", + args: []); + if (params.onWebViewCreated != null) { + params.onWebViewCreated!( + params.controllerFromPlatform?.call(_controller!) ?? _controller!); + } + } + + void _inferInitialSettings(InAppWebViewSettings settings) { + if (params.shouldOverrideUrlLoading != null && + settings.useShouldOverrideUrlLoading == null) { + settings.useShouldOverrideUrlLoading = true; + } + if (params.onLoadResource != null && settings.useOnLoadResource == null) { + settings.useOnLoadResource = true; + } + if (params.onDownloadStartRequest != null && + settings.useOnDownloadStart == null) { + settings.useOnDownloadStart = true; + } + if ((params.shouldInterceptAjaxRequest != null || + params.onAjaxProgress != null || + params.onAjaxReadyStateChange != null) && + settings.useShouldInterceptAjaxRequest == null) { + settings.useShouldInterceptAjaxRequest = true; + } + if (params.shouldInterceptFetchRequest != null && + settings.useShouldInterceptFetchRequest == null) { + settings.useShouldInterceptFetchRequest = true; + } + if (params.shouldInterceptRequest != null && + settings.useShouldInterceptRequest == null) { + settings.useShouldInterceptRequest = true; + } + if (params.onRenderProcessGone != null && + settings.useOnRenderProcessGone == null) { + settings.useOnRenderProcessGone = true; + } + if (params.onNavigationResponse != null && + settings.useOnNavigationResponse == null) { + settings.useOnNavigationResponse = true; + } + } + + @override + void dispose() { + dynamic viewId = _controller?.getViewId(); + debugLog( + className: runtimeType.toString(), + id: viewId?.toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: "dispose", + args: []); + final isKeepAlive = params.keepAlive != null; + _controller?.dispose(isKeepAlive: isKeepAlive); + _controller = null; + params.pullToRefreshController?.dispose(isKeepAlive: isKeepAlive); + params.findInteractionController?.dispose(isKeepAlive: isKeepAlive); + } + + @override + T controllerFromPlatform(PlatformInAppWebViewController controller) { + // unused + throw UnimplementedError(); + } +} diff --git a/packages/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview_controller.dart b/packages/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview_controller.dart new file mode 100644 index 0000000..ceb4bc9 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview_controller.dart @@ -0,0 +1,2779 @@ +import 'dart:io'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:core'; +import 'dart:developer' as developer; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../web_message/main.dart'; + +import '../in_app_browser/in_app_browser.dart'; +import '../web_storage/web_storage.dart'; + +import 'headless_in_app_webview.dart'; +import '_static_channel.dart'; + +import '../print_job/main.dart'; + +///List of forbidden names for JavaScript handlers. +// ignore: non_constant_identifier_names +final _JAVASCRIPT_HANDLER_FORBIDDEN_NAMES = UnmodifiableListView([ + "onLoadResource", + "shouldInterceptAjaxRequest", + "onAjaxReadyStateChange", + "onAjaxProgress", + "shouldInterceptFetchRequest", + "onPrintRequest", + "onWindowFocus", + "onWindowBlur", + "callAsyncJavaScript", + "evaluateJavaScriptWithContentWorld" +]); + +/// Object specifying creation parameters for creating a [AndroidInAppWebViewController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformInAppWebViewControllerCreationParams] for +/// more information. +@immutable +class AndroidInAppWebViewControllerCreationParams + extends PlatformInAppWebViewControllerCreationParams { + /// Creates a new [AndroidInAppWebViewControllerCreationParams] instance. + const AndroidInAppWebViewControllerCreationParams( + {required super.id, super.webviewParams}); + + /// Creates a [AndroidInAppWebViewControllerCreationParams] instance based on [PlatformInAppWebViewControllerCreationParams]. + factory AndroidInAppWebViewControllerCreationParams.fromPlatformInAppWebViewControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformInAppWebViewControllerCreationParams params) { + return AndroidInAppWebViewControllerCreationParams( + id: params.id, webviewParams: params.webviewParams); + } +} + +///Controls a WebView, such as an [InAppWebView] widget instance, a [AndroidHeadlessInAppWebView] instance or [AndroidInAppBrowser] WebView instance. +/// +///If you are using the [InAppWebView] widget, an [InAppWebViewController] instance can be obtained by setting the [InAppWebView.onWebViewCreated] +///callback. Instead, if you are using an [AndroidInAppBrowser] instance, you can get it through the [AndroidInAppBrowser.webViewController] attribute. +class AndroidInAppWebViewController extends PlatformInAppWebViewController + with ChannelController { + static final MethodChannel _staticChannel = IN_APP_WEBVIEW_STATIC_CHANNEL; + + // List of properties to be saved and restored for keep alive feature + Map _javaScriptHandlersMap = + HashMap(); + Map> _userScripts = { + UserScriptInjectionTime.AT_DOCUMENT_START: [], + UserScriptInjectionTime.AT_DOCUMENT_END: [] + }; + Set _webMessageListenerObjNames = Set(); + Map _injectedScriptsFromURL = {}; + Set _webMessageChannels = Set(); + Set _webMessageListeners = Set(); + + // static map that contains the properties to be saved and restored for keep alive feature + static final Map + _keepAliveMap = {}; + + AndroidInAppBrowser? _inAppBrowser; + + PlatformInAppBrowserEvents? get _inAppBrowserEventHandler => + _inAppBrowser?.eventHandler; + + dynamic _controllerFromPlatform; + + @override + late AndroidWebStorage webStorage; + + AndroidInAppWebViewController( + PlatformInAppWebViewControllerCreationParams params) + : super.implementation(params + is AndroidInAppWebViewControllerCreationParams + ? params + : AndroidInAppWebViewControllerCreationParams + .fromPlatformInAppWebViewControllerCreationParams(params)) { + channel = MethodChannel('com.pichillilorenzo/flutter_inappwebview_$id'); + handler = handleMethod; + initMethodCallHandler(); + + final initialUserScripts = webviewParams?.initialUserScripts; + if (initialUserScripts != null) { + for (final userScript in initialUserScripts) { + if (userScript.injectionTime == + UserScriptInjectionTime.AT_DOCUMENT_START) { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] + ?.add(userScript); + } else { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] + ?.add(userScript); + } + } + } + + this._init(params); + } + + static final AndroidInAppWebViewController _staticValue = + AndroidInAppWebViewController( + AndroidInAppWebViewControllerCreationParams(id: null)); + + factory AndroidInAppWebViewController.static() { + return _staticValue; + } + + AndroidInAppWebViewController.fromInAppBrowser( + PlatformInAppWebViewControllerCreationParams params, + MethodChannel channel, + AndroidInAppBrowser inAppBrowser, + UnmodifiableListView? initialUserScripts) + : super.implementation( + params is AndroidInAppWebViewControllerCreationParams + ? params + : AndroidInAppWebViewControllerCreationParams + .fromPlatformInAppWebViewControllerCreationParams(params)) { + this.channel = channel; + this._inAppBrowser = inAppBrowser; + + if (initialUserScripts != null) { + for (final userScript in initialUserScripts) { + if (userScript.injectionTime == + UserScriptInjectionTime.AT_DOCUMENT_START) { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] + ?.add(userScript); + } else { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] + ?.add(userScript); + } + } + } + this._init(params); + } + + void _init(PlatformInAppWebViewControllerCreationParams params) { + _controllerFromPlatform = + params.webviewParams?.controllerFromPlatform?.call(this) ?? this; + + webStorage = AndroidWebStorage(AndroidWebStorageCreationParams( + localStorage: AndroidLocalStorage.defaultStorage(controller: this), + sessionStorage: + AndroidSessionStorage.defaultStorage(controller: this))); + + if (params.webviewParams is PlatformInAppWebViewWidgetCreationParams) { + final keepAlive = + (params.webviewParams as PlatformInAppWebViewWidgetCreationParams) + .keepAlive; + if (keepAlive != null) { + InAppWebViewControllerKeepAliveProps? props = _keepAliveMap[keepAlive]; + if (props == null) { + // save controller properties to restore it later + _keepAliveMap[keepAlive] = InAppWebViewControllerKeepAliveProps( + injectedScriptsFromURL: _injectedScriptsFromURL, + javaScriptHandlersMap: _javaScriptHandlersMap, + userScripts: _userScripts, + webMessageListenerObjNames: _webMessageListenerObjNames, + webMessageChannels: _webMessageChannels, + webMessageListeners: _webMessageListeners); + } else { + // restore controller properties + _injectedScriptsFromURL = props.injectedScriptsFromURL; + _javaScriptHandlersMap = props.javaScriptHandlersMap; + _userScripts = props.userScripts; + _webMessageListenerObjNames = props.webMessageListenerObjNames; + _webMessageChannels = + props.webMessageChannels as Set; + _webMessageListeners = + props.webMessageListeners as Set; + } + } + } + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + name: _inAppBrowser == null + ? "WebView" + : _inAppBrowser.runtimeType.toString(), + id: (getViewId() ?? _inAppBrowser?.id).toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + if (PlatformInAppWebViewController.debugLoggingSettings.enabled && + call.method != "onCallJsHandler") { + _debugLog(call.method, call.arguments); + } + + switch (call.method) { + case "onLoadStart": + _injectedScriptsFromURL.clear(); + if ((webviewParams != null && webviewParams!.onLoadStart != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && webviewParams!.onLoadStart != null) + webviewParams!.onLoadStart!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onLoadStart(uri); + } + break; + case "onLoadStop": + if ((webviewParams != null && webviewParams!.onLoadStop != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && webviewParams!.onLoadStop != null) + webviewParams!.onLoadStop!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onLoadStop(uri); + } + break; + case "onReceivedError": + if ((webviewParams != null && + (webviewParams!.onReceivedError != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadError != null)) || + _inAppBrowserEventHandler != null) { + WebResourceRequest request = WebResourceRequest.fromMap( + call.arguments["request"].cast())!; + WebResourceError error = WebResourceError.fromMap( + call.arguments["error"].cast())!; + var isForMainFrame = request.isForMainFrame ?? false; + + if (webviewParams != null) { + if (webviewParams!.onReceivedError != null) + webviewParams!.onReceivedError!( + _controllerFromPlatform, request, error); + else if (isForMainFrame) { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadError!(_controllerFromPlatform, request.url, + error.type.toNativeValue() ?? -1, error.description); + } + } else { + if (isForMainFrame) { + _inAppBrowserEventHandler!.onLoadError(request.url, + error.type.toNativeValue() ?? -1, error.description); + } + _inAppBrowserEventHandler!.onReceivedError(request, error); + } + } + break; + case "onReceivedHttpError": + if ((webviewParams != null && + (webviewParams!.onReceivedHttpError != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadHttpError != null)) || + _inAppBrowserEventHandler != null) { + WebResourceRequest request = WebResourceRequest.fromMap( + call.arguments["request"].cast())!; + WebResourceResponse errorResponse = WebResourceResponse.fromMap( + call.arguments["errorResponse"].cast())!; + var isForMainFrame = request.isForMainFrame ?? false; + + if (webviewParams != null) { + if (webviewParams!.onReceivedHttpError != null) + webviewParams!.onReceivedHttpError!( + _controllerFromPlatform, request, errorResponse); + else if (isForMainFrame) { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadHttpError!( + _controllerFromPlatform, + request.url, + errorResponse.statusCode ?? -1, + errorResponse.reasonPhrase ?? ''); + } + } else { + if (isForMainFrame) { + _inAppBrowserEventHandler!.onLoadHttpError( + request.url, + errorResponse.statusCode ?? -1, + errorResponse.reasonPhrase ?? ''); + } + _inAppBrowserEventHandler! + .onReceivedHttpError(request, errorResponse); + } + } + break; + case "onProgressChanged": + if ((webviewParams != null && + webviewParams!.onProgressChanged != null) || + _inAppBrowserEventHandler != null) { + int progress = call.arguments["progress"]; + if (webviewParams != null && webviewParams!.onProgressChanged != null) + webviewParams!.onProgressChanged!( + _controllerFromPlatform, progress); + else + _inAppBrowserEventHandler!.onProgressChanged(progress); + } + break; + case "shouldOverrideUrlLoading": + if ((webviewParams != null && + webviewParams!.shouldOverrideUrlLoading != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + NavigationAction navigationAction = + NavigationAction.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldOverrideUrlLoading != null) + return (await webviewParams!.shouldOverrideUrlLoading!( + _controllerFromPlatform, navigationAction)) + ?.toNativeValue(); + return (await _inAppBrowserEventHandler! + .shouldOverrideUrlLoading(navigationAction)) + ?.toNativeValue(); + } + break; + case "onConsoleMessage": + if ((webviewParams != null && + webviewParams!.onConsoleMessage != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ConsoleMessage consoleMessage = ConsoleMessage.fromMap(arguments)!; + if (webviewParams != null && webviewParams!.onConsoleMessage != null) + webviewParams!.onConsoleMessage!( + _controllerFromPlatform, consoleMessage); + else + _inAppBrowserEventHandler!.onConsoleMessage(consoleMessage); + } + break; + case "onScrollChanged": + if ((webviewParams != null && webviewParams!.onScrollChanged != null) || + _inAppBrowserEventHandler != null) { + int x = call.arguments["x"]; + int y = call.arguments["y"]; + if (webviewParams != null && webviewParams!.onScrollChanged != null) + webviewParams!.onScrollChanged!(_controllerFromPlatform, x, y); + else + _inAppBrowserEventHandler!.onScrollChanged(x, y); + } + break; + case "onDownloadStartRequest": + if ((webviewParams != null && + // ignore: deprecated_member_use_from_same_package + (webviewParams!.onDownloadStart != null || + webviewParams!.onDownloadStartRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + DownloadStartRequest downloadStartRequest = + DownloadStartRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onDownloadStartRequest != null) + webviewParams!.onDownloadStartRequest!( + _controllerFromPlatform, downloadStartRequest); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onDownloadStart!( + _controllerFromPlatform, downloadStartRequest.url); + } + } else { + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .onDownloadStart(downloadStartRequest.url); + _inAppBrowserEventHandler! + .onDownloadStartRequest(downloadStartRequest); + } + } + break; + case "onLoadResourceWithCustomScheme": + if ((webviewParams != null && + (webviewParams!.onLoadResourceWithCustomScheme != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadResourceCustomScheme != null)) || + _inAppBrowserEventHandler != null) { + Map requestMap = + call.arguments["request"].cast(); + WebResourceRequest request = WebResourceRequest.fromMap(requestMap)!; + + if (webviewParams != null) { + if (webviewParams!.onLoadResourceWithCustomScheme != null) + return (await webviewParams!.onLoadResourceWithCustomScheme!( + _controllerFromPlatform, request)) + ?.toMap(); + else { + return (await params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .onLoadResourceCustomScheme!( + _controllerFromPlatform, request.url)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onLoadResourceWithCustomScheme(request)) ?? + (await _inAppBrowserEventHandler! + .onLoadResourceCustomScheme(request.url))) + ?.toMap(); + } + } + break; + case "onCreateWindow": + if ((webviewParams != null && webviewParams!.onCreateWindow != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + CreateWindowAction createWindowAction = + CreateWindowAction.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onCreateWindow != null) + return await webviewParams!.onCreateWindow!( + _controllerFromPlatform, createWindowAction); + else + return await _inAppBrowserEventHandler! + .onCreateWindow(createWindowAction); + } + break; + case "onCloseWindow": + if (webviewParams != null && webviewParams!.onCloseWindow != null) + webviewParams!.onCloseWindow!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onCloseWindow(); + break; + case "onTitleChanged": + if ((webviewParams != null && webviewParams!.onTitleChanged != null) || + _inAppBrowserEventHandler != null) { + String? title = call.arguments["title"]; + if (webviewParams != null && webviewParams!.onTitleChanged != null) + webviewParams!.onTitleChanged!(_controllerFromPlatform, title); + else + _inAppBrowserEventHandler!.onTitleChanged(title); + } + break; + case "onGeolocationPermissionsShowPrompt": + if ((webviewParams != null && + (webviewParams!.onGeolocationPermissionsShowPrompt != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsShowPrompt != + null)) || + _inAppBrowserEventHandler != null) { + String origin = call.arguments["origin"]; + + if (webviewParams != null) { + if (webviewParams!.onGeolocationPermissionsShowPrompt != null) + return (await webviewParams!.onGeolocationPermissionsShowPrompt!( + _controllerFromPlatform, origin)) + ?.toMap(); + else { + return (await params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .androidOnGeolocationPermissionsShowPrompt!( + _controllerFromPlatform, origin)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onGeolocationPermissionsShowPrompt(origin)) ?? + (await _inAppBrowserEventHandler! + .androidOnGeolocationPermissionsShowPrompt(origin))) + ?.toMap(); + } + } + break; + case "onGeolocationPermissionsHidePrompt": + if (webviewParams != null && + (webviewParams!.onGeolocationPermissionsHidePrompt != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsHidePrompt != + null)) { + if (webviewParams!.onGeolocationPermissionsHidePrompt != null) + webviewParams! + .onGeolocationPermissionsHidePrompt!(_controllerFromPlatform); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsHidePrompt!( + _controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onGeolocationPermissionsHidePrompt(); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnGeolocationPermissionsHidePrompt(); + } + break; + case "shouldInterceptRequest": + if ((webviewParams != null && + (webviewParams!.shouldInterceptRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidShouldInterceptRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + WebResourceRequest request = WebResourceRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.shouldInterceptRequest != null) + return (await webviewParams!.shouldInterceptRequest!( + _controllerFromPlatform, request)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidShouldInterceptRequest!( + _controllerFromPlatform, request)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .shouldInterceptRequest(request)) ?? + (await _inAppBrowserEventHandler! + .androidShouldInterceptRequest(request))) + ?.toMap(); + } + } + break; + case "onRenderProcessUnresponsive": + if ((webviewParams != null && + (webviewParams!.onRenderProcessUnresponsive != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessUnresponsive != + null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessUnresponsive != null) + return (await webviewParams!.onRenderProcessUnresponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnRenderProcessUnresponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onRenderProcessUnresponsive(uri)) ?? + (await _inAppBrowserEventHandler! + .androidOnRenderProcessUnresponsive(uri))) + ?.toNativeValue(); + } + } + break; + case "onRenderProcessResponsive": + if ((webviewParams != null && + (webviewParams!.onRenderProcessResponsive != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessResponsive != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessResponsive != null) + return (await webviewParams!.onRenderProcessResponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnRenderProcessResponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onRenderProcessResponsive(uri)) ?? + (await _inAppBrowserEventHandler! + .androidOnRenderProcessResponsive(uri))) + ?.toNativeValue(); + } + } + break; + case "onRenderProcessGone": + if ((webviewParams != null && + (webviewParams!.onRenderProcessGone != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessGone != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + RenderProcessGoneDetail detail = + RenderProcessGoneDetail.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessGone != null) + webviewParams!.onRenderProcessGone!( + _controllerFromPlatform, detail); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessGone!( + _controllerFromPlatform, detail); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onRenderProcessGone(detail); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.androidOnRenderProcessGone(detail); + } + } + break; + case "onFormResubmission": + if ((webviewParams != null && + (webviewParams!.onFormResubmission != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnFormResubmission != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onFormResubmission != null) + return (await webviewParams!.onFormResubmission!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnFormResubmission!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onFormResubmission(uri)) ?? + // ignore: deprecated_member_use_from_same_package + (await _inAppBrowserEventHandler! + .androidOnFormResubmission(uri))) + ?.toNativeValue(); + } + } + break; + case "onZoomScaleChanged": + if ((webviewParams != null && + // ignore: deprecated_member_use_from_same_package + (webviewParams!.androidOnScaleChanged != null || + webviewParams!.onZoomScaleChanged != null)) || + _inAppBrowserEventHandler != null) { + double oldScale = call.arguments["oldScale"]; + double newScale = call.arguments["newScale"]; + + if (webviewParams != null) { + if (webviewParams!.onZoomScaleChanged != null) + webviewParams!.onZoomScaleChanged!( + _controllerFromPlatform, oldScale, newScale); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnScaleChanged!( + _controllerFromPlatform, oldScale, newScale); + } + } else { + _inAppBrowserEventHandler!.onZoomScaleChanged(oldScale, newScale); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnScaleChanged(oldScale, newScale); + } + } + break; + case "onReceivedIcon": + if ((webviewParams != null && + (webviewParams!.onReceivedIcon != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedIcon != null)) || + _inAppBrowserEventHandler != null) { + Uint8List icon = + Uint8List.fromList(call.arguments["icon"].cast()); + + if (webviewParams != null) { + if (webviewParams!.onReceivedIcon != null) + webviewParams!.onReceivedIcon!(_controllerFromPlatform, icon); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedIcon!( + _controllerFromPlatform, icon); + } + } else { + _inAppBrowserEventHandler!.onReceivedIcon(icon); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.androidOnReceivedIcon(icon); + } + } + break; + case "onReceivedTouchIconUrl": + if ((webviewParams != null && + (webviewParams!.onReceivedTouchIconUrl != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedTouchIconUrl != null)) || + _inAppBrowserEventHandler != null) { + String url = call.arguments["url"]; + bool precomposed = call.arguments["precomposed"]; + WebUri uri = WebUri(url); + + if (webviewParams != null) { + if (webviewParams!.onReceivedTouchIconUrl != null) + webviewParams!.onReceivedTouchIconUrl!( + _controllerFromPlatform, uri, precomposed); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedTouchIconUrl!( + _controllerFromPlatform, uri, precomposed); + } + } else { + _inAppBrowserEventHandler!.onReceivedTouchIconUrl(uri, precomposed); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnReceivedTouchIconUrl(uri, precomposed); + } + } + break; + case "onJsAlert": + if ((webviewParams != null && webviewParams!.onJsAlert != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsAlertRequest jsAlertRequest = JsAlertRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsAlert != null) + return (await webviewParams!.onJsAlert!( + _controllerFromPlatform, jsAlertRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler!.onJsAlert(jsAlertRequest)) + ?.toMap(); + } + break; + case "onJsConfirm": + if ((webviewParams != null && webviewParams!.onJsConfirm != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsConfirmRequest jsConfirmRequest = + JsConfirmRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsConfirm != null) + return (await webviewParams!.onJsConfirm!( + _controllerFromPlatform, jsConfirmRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onJsConfirm(jsConfirmRequest)) + ?.toMap(); + } + break; + case "onJsPrompt": + if ((webviewParams != null && webviewParams!.onJsPrompt != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsPromptRequest jsPromptRequest = JsPromptRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsPrompt != null) + return (await webviewParams!.onJsPrompt!( + _controllerFromPlatform, jsPromptRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onJsPrompt(jsPromptRequest)) + ?.toMap(); + } + break; + case "onJsBeforeUnload": + if ((webviewParams != null && + (webviewParams!.onJsBeforeUnload != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnJsBeforeUnload != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsBeforeUnloadRequest jsBeforeUnloadRequest = + JsBeforeUnloadRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onJsBeforeUnload != null) + return (await webviewParams!.onJsBeforeUnload!( + _controllerFromPlatform, jsBeforeUnloadRequest)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnJsBeforeUnload!( + _controllerFromPlatform, jsBeforeUnloadRequest)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onJsBeforeUnload(jsBeforeUnloadRequest)) ?? + (await _inAppBrowserEventHandler! + .androidOnJsBeforeUnload(jsBeforeUnloadRequest))) + ?.toMap(); + } + } + break; + case "onSafeBrowsingHit": + if ((webviewParams != null && + (webviewParams!.onSafeBrowsingHit != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnSafeBrowsingHit != null)) || + _inAppBrowserEventHandler != null) { + String url = call.arguments["url"]; + SafeBrowsingThreat? threatType = + SafeBrowsingThreat.fromNativeValue(call.arguments["threatType"]); + WebUri uri = WebUri(url); + + if (webviewParams != null) { + if (webviewParams!.onSafeBrowsingHit != null) + return (await webviewParams!.onSafeBrowsingHit!( + _controllerFromPlatform, uri, threatType)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnSafeBrowsingHit!( + _controllerFromPlatform, uri, threatType)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onSafeBrowsingHit(uri, threatType)) ?? + (await _inAppBrowserEventHandler! + .androidOnSafeBrowsingHit(uri, threatType))) + ?.toMap(); + } + } + break; + case "onReceivedLoginRequest": + if ((webviewParams != null && + (webviewParams!.onReceivedLoginRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedLoginRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + LoginRequest loginRequest = LoginRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onReceivedLoginRequest != null) + webviewParams!.onReceivedLoginRequest!( + _controllerFromPlatform, loginRequest); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedLoginRequest!( + _controllerFromPlatform, loginRequest); + } + } else { + _inAppBrowserEventHandler!.onReceivedLoginRequest(loginRequest); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnReceivedLoginRequest(loginRequest); + } + } + break; + case "onPermissionRequestCanceled": + if ((webviewParams != null && + webviewParams!.onPermissionRequestCanceled != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + PermissionRequest permissionRequest = + PermissionRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onPermissionRequestCanceled != null) + webviewParams!.onPermissionRequestCanceled!( + _controllerFromPlatform, permissionRequest); + else + _inAppBrowserEventHandler! + .onPermissionRequestCanceled(permissionRequest); + } + break; + case "onRequestFocus": + if ((webviewParams != null && webviewParams!.onRequestFocus != null) || + _inAppBrowserEventHandler != null) { + if (webviewParams != null && webviewParams!.onRequestFocus != null) + webviewParams!.onRequestFocus!(_controllerFromPlatform); + else + _inAppBrowserEventHandler!.onRequestFocus(); + } + break; + case "onReceivedHttpAuthRequest": + if ((webviewParams != null && + webviewParams!.onReceivedHttpAuthRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + HttpAuthenticationChallenge challenge = + HttpAuthenticationChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedHttpAuthRequest != null) + return (await webviewParams!.onReceivedHttpAuthRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedHttpAuthRequest(challenge)) + ?.toMap(); + } + break; + case "onReceivedServerTrustAuthRequest": + if ((webviewParams != null && + webviewParams!.onReceivedServerTrustAuthRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ServerTrustChallenge challenge = + ServerTrustChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedServerTrustAuthRequest != null) + return (await webviewParams!.onReceivedServerTrustAuthRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedServerTrustAuthRequest(challenge)) + ?.toMap(); + } + break; + case "onReceivedClientCertRequest": + if ((webviewParams != null && + webviewParams!.onReceivedClientCertRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ClientCertChallenge challenge = + ClientCertChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedClientCertRequest != null) + return (await webviewParams!.onReceivedClientCertRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedClientCertRequest(challenge)) + ?.toMap(); + } + break; + case "onFindResultReceived": + if ((webviewParams != null && + (webviewParams!.onFindResultReceived != null || + (webviewParams!.findInteractionController != null && + webviewParams!.findInteractionController!.params + .onFindResultReceived != + null))) || + _inAppBrowserEventHandler != null) { + int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; + int numberOfMatches = call.arguments["numberOfMatches"]; + bool isDoneCounting = call.arguments["isDoneCounting"]; + if (webviewParams != null) { + if (webviewParams!.findInteractionController != null && + webviewParams!.findInteractionController!.params + .onFindResultReceived != + null) + webviewParams! + .findInteractionController!.params.onFindResultReceived!( + webviewParams!.findInteractionController!, + activeMatchOrdinal, + numberOfMatches, + isDoneCounting); + else + webviewParams!.onFindResultReceived!(_controllerFromPlatform, + activeMatchOrdinal, numberOfMatches, isDoneCounting); + } else { + if (_inAppBrowser!.findInteractionController != null && + _inAppBrowser! + .findInteractionController!.onFindResultReceived != + null) + _inAppBrowser!.findInteractionController!.onFindResultReceived!( + webviewParams!.findInteractionController!, + activeMatchOrdinal, + numberOfMatches, + isDoneCounting); + else + _inAppBrowserEventHandler!.onFindResultReceived( + activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + } + break; + case "onPermissionRequest": + if ((webviewParams != null && + (webviewParams!.onPermissionRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnPermissionRequest != null)) || + _inAppBrowserEventHandler != null) { + String origin = call.arguments["origin"]; + List resources = call.arguments["resources"].cast(); + + Map arguments = + call.arguments.cast(); + PermissionRequest permissionRequest = + PermissionRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onPermissionRequest != null) + return (await webviewParams!.onPermissionRequest!( + _controllerFromPlatform, permissionRequest)) + ?.toMap(); + else { + return (await webviewParams!.androidOnPermissionRequest!( + _controllerFromPlatform, origin, resources)) + ?.toMap(); + } + } else { + return (await _inAppBrowserEventHandler! + .onPermissionRequest(permissionRequest)) + ?.toMap() ?? + (await _inAppBrowserEventHandler! + .androidOnPermissionRequest(origin, resources)) + ?.toMap(); + } + } + break; + case "onUpdateVisitedHistory": + if ((webviewParams != null && + webviewParams!.onUpdateVisitedHistory != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + bool? isReload = call.arguments["isReload"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && + webviewParams!.onUpdateVisitedHistory != null) + webviewParams!.onUpdateVisitedHistory!( + _controllerFromPlatform, uri, isReload); + else + _inAppBrowserEventHandler!.onUpdateVisitedHistory(uri, isReload); + } + break; + case "onWebContentProcessDidTerminate": + if (webviewParams != null && + (webviewParams!.onWebContentProcessDidTerminate != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosOnWebContentProcessDidTerminate != null)) { + if (webviewParams!.onWebContentProcessDidTerminate != null) + webviewParams! + .onWebContentProcessDidTerminate!(_controllerFromPlatform); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams! + .iosOnWebContentProcessDidTerminate!(_controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onWebContentProcessDidTerminate(); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.iosOnWebContentProcessDidTerminate(); + } + break; + case "onPageCommitVisible": + if ((webviewParams != null && + webviewParams!.onPageCommitVisible != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && + webviewParams!.onPageCommitVisible != null) + webviewParams!.onPageCommitVisible!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onPageCommitVisible(uri); + } + break; + case "onDidReceiveServerRedirectForProvisionalNavigation": + if (webviewParams != null && + (webviewParams! + .onDidReceiveServerRedirectForProvisionalNavigation != + null || + params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .iosOnDidReceiveServerRedirectForProvisionalNavigation != + null)) { + if (webviewParams! + .onDidReceiveServerRedirectForProvisionalNavigation != + null) + webviewParams!.onDidReceiveServerRedirectForProvisionalNavigation!( + _controllerFromPlatform); + else { + params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .iosOnDidReceiveServerRedirectForProvisionalNavigation!( + _controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler! + .onDidReceiveServerRedirectForProvisionalNavigation(); + _inAppBrowserEventHandler! + .iosOnDidReceiveServerRedirectForProvisionalNavigation(); + } + break; + case "onNavigationResponse": + if ((webviewParams != null && + (webviewParams!.onNavigationResponse != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosOnNavigationResponse != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + // ignore: deprecated_member_use_from_same_package + IOSWKNavigationResponse iosOnNavigationResponse = + // ignore: deprecated_member_use_from_same_package + IOSWKNavigationResponse.fromMap(arguments)!; + + NavigationResponse navigationResponse = + NavigationResponse.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onNavigationResponse != null) + return (await webviewParams!.onNavigationResponse!( + _controllerFromPlatform, navigationResponse)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.iosOnNavigationResponse!( + _controllerFromPlatform, iosOnNavigationResponse)) + ?.toNativeValue(); + } + } else { + return (await _inAppBrowserEventHandler! + .onNavigationResponse(navigationResponse)) + ?.toNativeValue() ?? + (await _inAppBrowserEventHandler! + .iosOnNavigationResponse(iosOnNavigationResponse)) + ?.toNativeValue(); + } + } + break; + case "shouldAllowDeprecatedTLS": + if ((webviewParams != null && + (webviewParams!.shouldAllowDeprecatedTLS != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosShouldAllowDeprecatedTLS != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + URLAuthenticationChallenge challenge = + URLAuthenticationChallenge.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.shouldAllowDeprecatedTLS != null) + return (await webviewParams!.shouldAllowDeprecatedTLS!( + _controllerFromPlatform, challenge)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.iosShouldAllowDeprecatedTLS!( + _controllerFromPlatform, challenge)) + ?.toNativeValue(); + } + } else { + return (await _inAppBrowserEventHandler! + .shouldAllowDeprecatedTLS(challenge)) + ?.toNativeValue() ?? + // ignore: deprecated_member_use_from_same_package + (await _inAppBrowserEventHandler! + .iosShouldAllowDeprecatedTLS(challenge)) + ?.toNativeValue(); + } + } + break; + case "onLongPressHitTestResult": + if ((webviewParams != null && + webviewParams!.onLongPressHitTestResult != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + InAppWebViewHitTestResult hitTestResult = + InAppWebViewHitTestResult.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onLongPressHitTestResult != null) + webviewParams!.onLongPressHitTestResult!( + _controllerFromPlatform, hitTestResult); + else + _inAppBrowserEventHandler!.onLongPressHitTestResult(hitTestResult); + } + break; + case "onCreateContextMenu": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null && contextMenu.onCreateContextMenu != null) { + Map arguments = + call.arguments.cast(); + InAppWebViewHitTestResult hitTestResult = + InAppWebViewHitTestResult.fromMap(arguments)!; + + contextMenu.onCreateContextMenu!(hitTestResult); + } + break; + case "onHideContextMenu": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null && contextMenu.onHideContextMenu != null) { + contextMenu.onHideContextMenu!(); + } + break; + case "onContextMenuActionItemClicked": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null) { + int? androidId = call.arguments["androidId"]; + String? iosId = call.arguments["iosId"]; + dynamic id = call.arguments["id"]; + String title = call.arguments["title"]; + + ContextMenuItem menuItemClicked = ContextMenuItem( + id: id, + // ignore: deprecated_member_use_from_same_package + androidId: androidId, + // ignore: deprecated_member_use_from_same_package + iosId: iosId, + title: title, + action: null); + + for (var menuItem in contextMenu.menuItems) { + if (menuItem.id == id) { + menuItemClicked = menuItem; + if (menuItem.action != null) { + menuItem.action!(); + } + break; + } + } + + if (contextMenu.onContextMenuActionItemClicked != null) { + contextMenu.onContextMenuActionItemClicked!(menuItemClicked); + } + } + break; + case "onEnterFullscreen": + if (webviewParams != null && webviewParams!.onEnterFullscreen != null) + webviewParams!.onEnterFullscreen!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onEnterFullscreen(); + break; + case "onExitFullscreen": + if (webviewParams != null && webviewParams!.onExitFullscreen != null) + webviewParams!.onExitFullscreen!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onExitFullscreen(); + break; + case "onOverScrolled": + if ((webviewParams != null && webviewParams!.onOverScrolled != null) || + _inAppBrowserEventHandler != null) { + int x = call.arguments["x"]; + int y = call.arguments["y"]; + bool clampedX = call.arguments["clampedX"]; + bool clampedY = call.arguments["clampedY"]; + + if (webviewParams != null && webviewParams!.onOverScrolled != null) + webviewParams!.onOverScrolled!( + _controllerFromPlatform, x, y, clampedX, clampedY); + else + _inAppBrowserEventHandler!.onOverScrolled(x, y, clampedX, clampedY); + } + break; + case "onWindowFocus": + if (webviewParams != null && webviewParams!.onWindowFocus != null) + webviewParams!.onWindowFocus!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowFocus(); + break; + case "onWindowBlur": + if (webviewParams != null && webviewParams!.onWindowBlur != null) + webviewParams!.onWindowBlur!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowBlur(); + break; + case "onPrintRequest": + if ((webviewParams != null && + (webviewParams!.onPrintRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onPrint != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + String? printJobId = call.arguments["printJobId"]; + WebUri? uri = url != null ? WebUri(url) : null; + AndroidPrintJobController? printJob = printJobId != null + ? AndroidPrintJobController( + AndroidPrintJobControllerCreationParams(id: printJobId)) + : null; + + if (webviewParams != null) { + if (webviewParams!.onPrintRequest != null) + return await webviewParams!.onPrintRequest!( + _controllerFromPlatform, uri, printJob); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onPrint!(_controllerFromPlatform, uri); + return false; + } + } else { + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.onPrint(uri); + return await _inAppBrowserEventHandler! + .onPrintRequest(uri, printJob); + } + } + break; + case "onInjectedScriptLoaded": + String id = call.arguments[0]; + var onLoadCallback = _injectedScriptsFromURL[id]?.onLoad; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onLoadCallback != null) { + onLoadCallback(); + } + break; + case "onInjectedScriptError": + String id = call.arguments[0]; + var onErrorCallback = _injectedScriptsFromURL[id]?.onError; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onErrorCallback != null) { + onErrorCallback(); + } + break; + case "onCameraCaptureStateChanged": + if ((webviewParams != null && + webviewParams!.onCameraCaptureStateChanged != null) || + _inAppBrowserEventHandler != null) { + var oldState = + MediaCaptureState.fromNativeValue(call.arguments["oldState"]); + var newState = + MediaCaptureState.fromNativeValue(call.arguments["newState"]); + + if (webviewParams != null && + webviewParams!.onCameraCaptureStateChanged != null) + webviewParams!.onCameraCaptureStateChanged!( + _controllerFromPlatform, oldState, newState); + else + _inAppBrowserEventHandler! + .onCameraCaptureStateChanged(oldState, newState); + } + break; + case "onMicrophoneCaptureStateChanged": + if ((webviewParams != null && + webviewParams!.onMicrophoneCaptureStateChanged != null) || + _inAppBrowserEventHandler != null) { + var oldState = + MediaCaptureState.fromNativeValue(call.arguments["oldState"]); + var newState = + MediaCaptureState.fromNativeValue(call.arguments["newState"]); + + if (webviewParams != null && + webviewParams!.onMicrophoneCaptureStateChanged != null) + webviewParams!.onMicrophoneCaptureStateChanged!( + _controllerFromPlatform, oldState, newState); + else + _inAppBrowserEventHandler! + .onMicrophoneCaptureStateChanged(oldState, newState); + } + break; + case "onContentSizeChanged": + if ((webviewParams != null && + webviewParams!.onContentSizeChanged != null) || + _inAppBrowserEventHandler != null) { + var oldContentSize = MapSize.fromMap( + call.arguments["oldContentSize"]?.cast())!; + var newContentSize = MapSize.fromMap( + call.arguments["newContentSize"]?.cast())!; + + if (webviewParams != null && + webviewParams!.onContentSizeChanged != null) + webviewParams!.onContentSizeChanged!( + _controllerFromPlatform, oldContentSize, newContentSize); + else + _inAppBrowserEventHandler! + .onContentSizeChanged(oldContentSize, newContentSize); + } + break; + case "onCallJsHandler": + String handlerName = call.arguments["handlerName"]; + // decode args to json + List args = jsonDecode(call.arguments["args"]); + + _debugLog(handlerName, args); + + switch (handlerName) { + case "onLoadResource": + if ((webviewParams != null && + webviewParams!.onLoadResource != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + arguments["startTime"] = arguments["startTime"] is int + ? arguments["startTime"].toDouble() + : arguments["startTime"]; + arguments["duration"] = arguments["duration"] is int + ? arguments["duration"].toDouble() + : arguments["duration"]; + + var response = LoadedResource.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onLoadResource != null) + webviewParams!.onLoadResource!( + _controllerFromPlatform, response); + else + _inAppBrowserEventHandler!.onLoadResource(response); + } + return null; + case "shouldInterceptAjaxRequest": + if ((webviewParams != null && + webviewParams!.shouldInterceptAjaxRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldInterceptAjaxRequest != null) + return jsonEncode( + await params.webviewParams!.shouldInterceptAjaxRequest!( + _controllerFromPlatform, request)); + else + return jsonEncode(await _inAppBrowserEventHandler! + .shouldInterceptAjaxRequest(request)); + } + return null; + case "onAjaxReadyStateChange": + if ((webviewParams != null && + webviewParams!.onAjaxReadyStateChange != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onAjaxReadyStateChange != null) + return (await webviewParams!.onAjaxReadyStateChange!( + _controllerFromPlatform, request)) + ?.toNativeValue(); + else + return (await _inAppBrowserEventHandler! + .onAjaxReadyStateChange(request)) + ?.toNativeValue(); + } + return null; + case "onAjaxProgress": + if ((webviewParams != null && + webviewParams!.onAjaxProgress != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onAjaxProgress != null) + return (await webviewParams!.onAjaxProgress!( + _controllerFromPlatform, request)) + ?.toNativeValue(); + else + return (await _inAppBrowserEventHandler! + .onAjaxProgress(request)) + ?.toNativeValue(); + } + return null; + case "shouldInterceptFetchRequest": + if ((webviewParams != null && + webviewParams!.shouldInterceptFetchRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + FetchRequest request = FetchRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldInterceptFetchRequest != null) + return jsonEncode( + await webviewParams!.shouldInterceptFetchRequest!( + _controllerFromPlatform, request)); + else + return jsonEncode(await _inAppBrowserEventHandler! + .shouldInterceptFetchRequest(request)); + } + return null; + case "onWindowFocus": + if (webviewParams != null && webviewParams!.onWindowFocus != null) + webviewParams!.onWindowFocus!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowFocus(); + return null; + case "onWindowBlur": + if (webviewParams != null && webviewParams!.onWindowBlur != null) + webviewParams!.onWindowBlur!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowBlur(); + return null; + case "onInjectedScriptLoaded": + String id = args[0]; + var onLoadCallback = _injectedScriptsFromURL[id]?.onLoad; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onLoadCallback != null) { + onLoadCallback(); + } + return null; + case "onInjectedScriptError": + String id = args[0]; + var onErrorCallback = _injectedScriptsFromURL[id]?.onError; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onErrorCallback != null) { + onErrorCallback(); + } + return null; + } + + if (_javaScriptHandlersMap.containsKey(handlerName)) { + // convert result to json + try { + return jsonEncode(await _javaScriptHandlersMap[handlerName]!(args)); + } catch (error, stacktrace) { + developer.log(error.toString() + '\n' + stacktrace.toString(), + name: 'JavaScript Handler "$handlerName"'); + throw Exception(error.toString().replaceFirst('Exception: ', '')); + } + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + Future getUrl() async { + Map args = {}; + String? url = await channel?.invokeMethod('getUrl', args); + return url != null ? WebUri(url) : null; + } + + @override + Future getTitle() async { + Map args = {}; + return await channel?.invokeMethod('getTitle', args); + } + + @override + Future getProgress() async { + Map args = {}; + return await channel?.invokeMethod('getProgress', args); + } + + @override + Future getHtml() async { + String? html; + + InAppWebViewSettings? settings = await getSettings(); + if (settings != null && settings.javaScriptEnabled == true) { + html = await evaluateJavascript( + source: "window.document.getElementsByTagName('html')[0].outerHTML;"); + if (html != null && html.isNotEmpty) return html; + } + + var webviewUrl = await getUrl(); + if (webviewUrl == null) { + return html; + } + + if (webviewUrl.isScheme("file")) { + var assetPathSplit = webviewUrl.toString().split("/flutter_assets/"); + var assetPath = assetPathSplit[assetPathSplit.length - 1]; + try { + var bytes = await rootBundle.load(assetPath); + html = utf8.decode(bytes.buffer.asUint8List()); + } catch (e) {} + } else { + try { + HttpClient client = HttpClient(); + var htmlRequest = await client.getUrl(webviewUrl); + html = + await (await htmlRequest.close()).transform(Utf8Decoder()).join(); + } catch (e) { + developer.log(e.toString(), name: this.runtimeType.toString()); + } + } + + return html; + } + + @override + Future> getFavicons() async { + List favicons = []; + + var webviewUrl = await getUrl(); + + if (webviewUrl == null) { + return favicons; + } + + String? manifestUrl; + + var html = await getHtml(); + if (html == null || html.isEmpty) { + return favicons; + } + var assetPathBase; + + if (webviewUrl.isScheme("file")) { + var assetPathSplit = webviewUrl.toString().split("/flutter_assets/"); + assetPathBase = assetPathSplit[0] + "/flutter_assets/"; + } + + InAppWebViewSettings? settings = await getSettings(); + if (settings != null && settings.javaScriptEnabled == true) { + List> links = (await evaluateJavascript(source: """ +(function() { + var linkNodes = document.head.getElementsByTagName("link"); + var links = []; + for (var i = 0; i < linkNodes.length; i++) { + var linkNode = linkNodes[i]; + if (linkNode.rel === 'manifest') { + links.push( + { + rel: linkNode.rel, + href: linkNode.href, + sizes: null + } + ); + } else if (linkNode.rel != null && linkNode.rel.indexOf('icon') >= 0) { + links.push( + { + rel: linkNode.rel, + href: linkNode.href, + sizes: linkNode.sizes != null && linkNode.sizes.value != "" ? linkNode.sizes.value : null + } + ); + } + } + return links; +})(); +"""))?.cast>() ?? []; + for (var link in links) { + if (link["rel"] == "manifest") { + manifestUrl = link["href"]; + if (!_isUrlAbsolute(manifestUrl!)) { + if (manifestUrl.startsWith("/")) { + manifestUrl = manifestUrl.substring(1); + } + manifestUrl = ((assetPathBase == null) + ? webviewUrl.scheme + "://" + webviewUrl.host + "/" + : assetPathBase) + + manifestUrl; + } + continue; + } + favicons.addAll(_createFavicons(webviewUrl, assetPathBase, link["href"], + link["rel"], link["sizes"], false)); + } + } + + // try to get /favicon.ico + try { + HttpClient client = HttpClient(); + var faviconUrl = + webviewUrl.scheme + "://" + webviewUrl.host + "/favicon.ico"; + var faviconUri = WebUri(faviconUrl); + var headRequest = await client.headUrl(faviconUri); + var headResponse = await headRequest.close(); + if (headResponse.statusCode == 200) { + favicons.add(Favicon(url: faviconUri, rel: "shortcut icon")); + } + } catch (e) { + developer.log("/favicon.ico file not found: " + e.toString(), + name: this.runtimeType.toString()); + } + + // try to get the manifest file + HttpClientRequest? manifestRequest; + HttpClientResponse? manifestResponse; + bool manifestFound = false; + if (manifestUrl == null) { + manifestUrl = + webviewUrl.scheme + "://" + webviewUrl.host + "/manifest.json"; + } + try { + HttpClient client = HttpClient(); + manifestRequest = await client.getUrl(Uri.parse(manifestUrl)); + manifestResponse = await manifestRequest.close(); + manifestFound = manifestResponse.statusCode == 200 && + manifestResponse.headers.contentType?.mimeType == "application/json"; + } catch (e) { + developer.log("Manifest file not found: " + e.toString(), + name: runtimeType.toString()); + } + + if (manifestFound) { + try { + Map manifest = json + .decode(await manifestResponse!.transform(Utf8Decoder()).join()); + if (manifest.containsKey("icons")) { + for (Map icon in manifest["icons"]) { + favicons.addAll(_createFavicons(webviewUrl, assetPathBase, + icon["src"], icon["rel"], icon["sizes"], true)); + } + } + } catch (e) { + developer.log( + "Cannot get favicons from Manifest file. It might not have a valid format: " + + e.toString(), + error: e, + name: runtimeType.toString()); + } + } + + return favicons; + } + + bool _isUrlAbsolute(String url) { + return url.startsWith("http://") || url.startsWith("https://"); + } + + List _createFavicons(WebUri url, String? assetPathBase, + String urlIcon, String? rel, String? sizes, bool isManifest) { + List favicons = []; + + List urlSplit = urlIcon.split("/"); + if (!_isUrlAbsolute(urlIcon)) { + if (urlIcon.startsWith("/")) { + urlIcon = urlIcon.substring(1); + } + urlIcon = ((assetPathBase == null) + ? url.scheme + "://" + url.host + "/" + : assetPathBase) + + urlIcon; + } + if (isManifest) { + rel = (sizes != null) + ? urlSplit[urlSplit.length - 1] + .replaceFirst("-" + sizes, "") + .split(" ")[0] + .split(".")[0] + : null; + } + if (sizes != null && sizes.isNotEmpty && sizes != "any") { + List sizesSplit = sizes.split(" "); + for (String size in sizesSplit) { + int width = int.parse(size.split("x")[0]); + int height = int.parse(size.split("x")[1]); + favicons.add(Favicon( + url: WebUri(urlIcon), rel: rel, width: width, height: height)); + } + } else { + favicons.add( + Favicon(url: WebUri(urlIcon), rel: rel, width: null, height: null)); + } + + return favicons; + } + + @override + Future loadUrl( + {required URLRequest urlRequest, + @Deprecated('Use allowingReadAccessTo instead') + Uri? iosAllowingReadAccessTo, + WebUri? allowingReadAccessTo}) async { + assert(urlRequest.url != null && urlRequest.url.toString().isNotEmpty); + assert( + allowingReadAccessTo == null || allowingReadAccessTo.isScheme("file")); + assert(iosAllowingReadAccessTo == null || + iosAllowingReadAccessTo.isScheme("file")); + + Map args = {}; + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + args.putIfAbsent( + 'allowingReadAccessTo', + () => + allowingReadAccessTo?.toString() ?? + iosAllowingReadAccessTo?.toString()); + await channel?.invokeMethod('loadUrl', args); + } + + @override + Future postUrl( + {required WebUri url, required Uint8List postData}) async { + assert(url.toString().isNotEmpty); + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('postData', () => postData); + await channel?.invokeMethod('postUrl', args); + } + + @override + Future loadData( + {required String data, + String mimeType = "text/html", + String encoding = "utf8", + WebUri? baseUrl, + @Deprecated('Use historyUrl instead') Uri? androidHistoryUrl, + WebUri? historyUrl, + @Deprecated('Use allowingReadAccessTo instead') + Uri? iosAllowingReadAccessTo, + WebUri? allowingReadAccessTo}) async { + assert( + allowingReadAccessTo == null || allowingReadAccessTo.isScheme("file")); + assert(iosAllowingReadAccessTo == null || + iosAllowingReadAccessTo.isScheme("file")); + + Map args = {}; + args.putIfAbsent('data', () => data); + args.putIfAbsent('mimeType', () => mimeType); + args.putIfAbsent('encoding', () => encoding); + args.putIfAbsent('baseUrl', () => baseUrl?.toString() ?? "about:blank"); + args.putIfAbsent( + 'historyUrl', + () => + historyUrl?.toString() ?? + androidHistoryUrl?.toString() ?? + "about:blank"); + args.putIfAbsent( + 'allowingReadAccessTo', + () => + allowingReadAccessTo?.toString() ?? + iosAllowingReadAccessTo?.toString()); + await channel?.invokeMethod('loadData', args); + } + + @override + Future loadFile({required String assetFilePath}) async { + assert(assetFilePath.isNotEmpty); + Map args = {}; + args.putIfAbsent('assetFilePath', () => assetFilePath); + await channel?.invokeMethod('loadFile', args); + } + + @override + Future reload() async { + Map args = {}; + await channel?.invokeMethod('reload', args); + } + + @override + Future goBack() async { + Map args = {}; + await channel?.invokeMethod('goBack', args); + } + + @override + Future canGoBack() async { + Map args = {}; + return await channel?.invokeMethod('canGoBack', args) ?? false; + } + + @override + Future goForward() async { + Map args = {}; + await channel?.invokeMethod('goForward', args); + } + + @override + Future canGoForward() async { + Map args = {}; + return await channel?.invokeMethod('canGoForward', args) ?? false; + } + + @override + Future goBackOrForward({required int steps}) async { + Map args = {}; + args.putIfAbsent('steps', () => steps); + await channel?.invokeMethod('goBackOrForward', args); + } + + @override + Future canGoBackOrForward({required int steps}) async { + Map args = {}; + args.putIfAbsent('steps', () => steps); + return await channel?.invokeMethod('canGoBackOrForward', args) ?? + false; + } + + @override + Future goTo({required WebHistoryItem historyItem}) async { + var steps = historyItem.offset; + if (steps != null) { + await goBackOrForward(steps: steps); + } + } + + @override + Future isLoading() async { + Map args = {}; + return await channel?.invokeMethod('isLoading', args) ?? false; + } + + @override + Future stopLoading() async { + Map args = {}; + await channel?.invokeMethod('stopLoading', args); + } + + @override + Future evaluateJavascript( + {required String source, ContentWorld? contentWorld}) async { + Map args = {}; + args.putIfAbsent('source', () => source); + args.putIfAbsent('contentWorld', () => contentWorld?.toMap()); + var data = await channel?.invokeMethod('evaluateJavascript', args); + if (data != null) { + try { + // try to json decode the data coming from JavaScript + // otherwise return it as it is. + data = json.decode(data); + } catch (e) {} + } + return data; + } + + @override + Future injectJavascriptFileFromUrl( + {required WebUri urlFile, + ScriptHtmlTagAttributes? scriptHtmlTagAttributes}) async { + assert(urlFile.toString().isNotEmpty); + var id = scriptHtmlTagAttributes?.id; + if (scriptHtmlTagAttributes != null && id != null) { + _injectedScriptsFromURL[id] = scriptHtmlTagAttributes; + } + Map args = {}; + args.putIfAbsent('urlFile', () => urlFile.toString()); + args.putIfAbsent( + 'scriptHtmlTagAttributes', () => scriptHtmlTagAttributes?.toMap()); + await channel?.invokeMethod('injectJavascriptFileFromUrl', args); + } + + @override + Future injectJavascriptFileFromAsset( + {required String assetFilePath}) async { + String source = await rootBundle.loadString(assetFilePath); + return await evaluateJavascript(source: source); + } + + @override + Future injectCSSCode({required String source}) async { + Map args = {}; + args.putIfAbsent('source', () => source); + await channel?.invokeMethod('injectCSSCode', args); + } + + @override + Future injectCSSFileFromUrl( + {required WebUri urlFile, + CSSLinkHtmlTagAttributes? cssLinkHtmlTagAttributes}) async { + assert(urlFile.toString().isNotEmpty); + Map args = {}; + args.putIfAbsent('urlFile', () => urlFile.toString()); + args.putIfAbsent( + 'cssLinkHtmlTagAttributes', () => cssLinkHtmlTagAttributes?.toMap()); + await channel?.invokeMethod('injectCSSFileFromUrl', args); + } + + @override + Future injectCSSFileFromAsset({required String assetFilePath}) async { + String source = await rootBundle.loadString(assetFilePath); + await injectCSSCode(source: source); + } + + @override + void addJavaScriptHandler( + {required String handlerName, + required JavaScriptHandlerCallback callback}) { + assert(!_JAVASCRIPT_HANDLER_FORBIDDEN_NAMES.contains(handlerName), + '"$handlerName" is a forbidden name!'); + this._javaScriptHandlersMap[handlerName] = (callback); + } + + @override + JavaScriptHandlerCallback? removeJavaScriptHandler( + {required String handlerName}) { + return this._javaScriptHandlersMap.remove(handlerName); + } + + @override + bool hasJavaScriptHandler({required String handlerName}) { + return this._javaScriptHandlersMap.containsKey(handlerName); + } + + @override + Future takeScreenshot( + {ScreenshotConfiguration? screenshotConfiguration}) async { + Map args = {}; + args.putIfAbsent( + 'screenshotConfiguration', () => screenshotConfiguration?.toMap()); + return await channel?.invokeMethod('takeScreenshot', args); + } + + @override + @Deprecated('Use setSettings instead') + Future setOptions({required InAppWebViewGroupOptions options}) async { + InAppWebViewSettings settings = + InAppWebViewSettings.fromMap(options.toMap()) ?? InAppWebViewSettings(); + await setSettings(settings: settings); + } + + @override + @Deprecated('Use getSettings instead') + Future getOptions() async { + InAppWebViewSettings? settings = await getSettings(); + + Map? options = settings?.toMap(); + if (options != null) { + options = options.cast(); + return InAppWebViewGroupOptions.fromMap(options as Map); + } + + return null; + } + + @override + Future setSettings({required InAppWebViewSettings settings}) async { + Map args = {}; + + args.putIfAbsent('settings', () => settings.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + Future getSettings() async { + Map args = {}; + + Map? settings = + await channel?.invokeMethod('getSettings', args); + if (settings != null) { + settings = settings.cast(); + return InAppWebViewSettings.fromMap(settings as Map); + } + + return null; + } + + @override + Future getCopyBackForwardList() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('getCopyBackForwardList', args)) + ?.cast(); + return WebHistory.fromMap(result); + } + + @override + @Deprecated("Use InAppWebViewController.clearAllCache instead") + Future clearCache() async { + Map args = {}; + await channel?.invokeMethod('clearCache', args); + } + + @override + @Deprecated("Use FindInteractionController.findAll instead") + Future findAllAsync({required String find}) async { + Map args = {}; + args.putIfAbsent('find', () => find); + await channel?.invokeMethod('findAll', args); + } + + @override + @Deprecated("Use FindInteractionController.findNext instead") + Future findNext({required bool forward}) async { + Map args = {}; + args.putIfAbsent('forward', () => forward); + await channel?.invokeMethod('findNext', args); + } + + @override + @Deprecated("Use FindInteractionController.clearMatches instead") + Future clearMatches() async { + Map args = {}; + await channel?.invokeMethod('clearMatches', args); + } + + @override + @Deprecated("Use tRexRunnerHtml instead") + Future getTRexRunnerHtml() async { + return await tRexRunnerHtml; + } + + @override + @Deprecated("Use tRexRunnerCss instead") + Future getTRexRunnerCss() async { + return await tRexRunnerCss; + } + + @override + Future scrollTo( + {required int x, required int y, bool animated = false}) async { + Map args = {}; + args.putIfAbsent('x', () => x); + args.putIfAbsent('y', () => y); + args.putIfAbsent('animated', () => animated); + await channel?.invokeMethod('scrollTo', args); + } + + @override + Future scrollBy( + {required int x, required int y, bool animated = false}) async { + Map args = {}; + args.putIfAbsent('x', () => x); + args.putIfAbsent('y', () => y); + args.putIfAbsent('animated', () => animated); + await channel?.invokeMethod('scrollBy', args); + } + + @override + Future pauseTimers() async { + Map args = {}; + await channel?.invokeMethod('pauseTimers', args); + } + + @override + Future resumeTimers() async { + Map args = {}; + await channel?.invokeMethod('resumeTimers', args); + } + + @override + Future printCurrentPage( + {PrintJobSettings? settings}) async { + Map args = {}; + args.putIfAbsent("settings", () => settings?.toMap()); + String? jobId = + await channel?.invokeMethod('printCurrentPage', args); + if (jobId != null) { + return AndroidPrintJobController( + PlatformPrintJobControllerCreationParams(id: jobId)); + } + return null; + } + + @override + Future getContentHeight() async { + Map args = {}; + var height = await channel?.invokeMethod('getContentHeight', args); + if (height == null || height == 0) { + // try to use javascript + var scrollHeight = await evaluateJavascript( + source: "document.documentElement.scrollHeight;"); + if (scrollHeight != null && scrollHeight is num) { + height = scrollHeight.toInt(); + } + } + return height; + } + + @override + Future getContentWidth() async { + Map args = {}; + var height = await channel?.invokeMethod('getContentWidth', args); + if (height == null || height == 0) { + // try to use javascript + var scrollHeight = await evaluateJavascript( + source: "document.documentElement.scrollWidth;"); + if (scrollHeight != null && scrollHeight is num) { + height = scrollHeight.toInt(); + } + } + return height; + } + + @override + Future zoomBy( + {required double zoomFactor, + @Deprecated('Use animated instead') bool? iosAnimated, + bool animated = false}) async { + assert(zoomFactor > 0.01 && zoomFactor <= 100.0); + + Map args = {}; + args.putIfAbsent('zoomFactor', () => zoomFactor); + args.putIfAbsent('animated', () => iosAnimated ?? animated); + return await channel?.invokeMethod('zoomBy', args); + } + + @override + Future getOriginalUrl() async { + Map args = {}; + String? url = await channel?.invokeMethod('getOriginalUrl', args); + return url != null ? WebUri(url) : null; + } + + @override + Future getZoomScale() async { + Map args = {}; + return await channel?.invokeMethod('getZoomScale', args); + } + + @override + @Deprecated('Use getZoomScale instead') + Future getScale() async { + return await getZoomScale(); + } + + @override + Future getSelectedText() async { + Map args = {}; + return await channel?.invokeMethod('getSelectedText', args); + } + + @override + Future getHitTestResult() async { + Map args = {}; + Map? hitTestResultMap = + await channel?.invokeMethod('getHitTestResult', args); + + if (hitTestResultMap == null) { + return null; + } + + hitTestResultMap = hitTestResultMap.cast(); + + InAppWebViewHitTestResultType? type = + InAppWebViewHitTestResultType.fromNativeValue( + hitTestResultMap["type"]?.toInt()); + String? extra = hitTestResultMap["extra"]; + return InAppWebViewHitTestResult(type: type, extra: extra); + } + + @override + Future clearFocus() async { + Map args = {}; + return await channel?.invokeMethod('clearFocus', args); + } + + @override + Future setContextMenu(ContextMenu? contextMenu) async { + Map args = {}; + args.putIfAbsent("contextMenu", () => contextMenu?.toMap()); + await channel?.invokeMethod('setContextMenu', args); + _inAppBrowser?.setContextMenu(contextMenu); + } + + @override + Future requestFocusNodeHref() async { + Map args = {}; + Map? result = + await channel?.invokeMethod('requestFocusNodeHref', args); + return result != null + ? RequestFocusNodeHrefResult( + url: result['url'] != null ? WebUri(result['url']) : null, + title: result['title'], + src: result['src'], + ) + : null; + } + + @override + Future requestImageRef() async { + Map args = {}; + Map? result = + await channel?.invokeMethod('requestImageRef', args); + return result != null + ? RequestImageRefResult( + url: result['url'] != null ? WebUri(result['url']) : null, + ) + : null; + } + + @override + Future> getMetaTags() async { + List metaTags = []; + + List>? metaTagList = + (await evaluateJavascript(source: """ +(function() { + var metaTags = []; + var metaTagNodes = document.head.getElementsByTagName('meta'); + for (var i = 0; i < metaTagNodes.length; i++) { + var metaTagNode = metaTagNodes[i]; + + var otherAttributes = metaTagNode.getAttributeNames(); + var nameIndex = otherAttributes.indexOf("name"); + if (nameIndex !== -1) otherAttributes.splice(nameIndex, 1); + var contentIndex = otherAttributes.indexOf("content"); + if (contentIndex !== -1) otherAttributes.splice(contentIndex, 1); + + var attrs = []; + for (var j = 0; j < otherAttributes.length; j++) { + var otherAttribute = otherAttributes[j]; + attrs.push( + { + name: otherAttribute, + value: metaTagNode.getAttribute(otherAttribute) + } + ); + } + + metaTags.push( + { + name: metaTagNode.name, + content: metaTagNode.content, + attrs: attrs + } + ); + } + return metaTags; +})(); + """))?.cast>(); + + if (metaTagList == null) { + return metaTags; + } + + for (var metaTag in metaTagList) { + var attrs = []; + + for (var metaTagAttr in metaTag["attrs"]) { + attrs.add(MetaTagAttribute( + name: metaTagAttr["name"], value: metaTagAttr["value"])); + } + + metaTags.add(MetaTag( + name: metaTag["name"], content: metaTag["content"], attrs: attrs)); + } + + return metaTags; + } + + @override + Future getMetaThemeColor() async { + Color? themeColor; + + try { + Map args = {}; + themeColor = UtilColor.fromStringRepresentation( + await channel?.invokeMethod('getMetaThemeColor', args)); + return themeColor; + } catch (e) { + // not implemented + } + + // try using javascript + var metaTags = await getMetaTags(); + MetaTag? metaTagThemeColor; + + for (var metaTag in metaTags) { + if (metaTag.name == "theme-color") { + metaTagThemeColor = metaTag; + break; + } + } + + if (metaTagThemeColor == null) { + return null; + } + + var colorValue = metaTagThemeColor.content; + + themeColor = colorValue != null + ? UtilColor.fromStringRepresentation(colorValue) + : null; + + return themeColor; + } + + @override + Future getScrollX() async { + Map args = {}; + return await channel?.invokeMethod('getScrollX', args); + } + + @override + Future getScrollY() async { + Map args = {}; + return await channel?.invokeMethod('getScrollY', args); + } + + @override + Future getCertificate() async { + Map args = {}; + Map? sslCertificateMap = + (await channel?.invokeMethod('getCertificate', args)) + ?.cast(); + return SslCertificate.fromMap(sslCertificateMap); + } + + @override + Future addUserScript({required UserScript userScript}) async { + Map args = {}; + args.putIfAbsent('userScript', () => userScript.toMap()); + if (!(_userScripts[userScript.injectionTime]?.contains(userScript) ?? + false)) { + _userScripts[userScript.injectionTime]?.add(userScript); + await channel?.invokeMethod('addUserScript', args); + } + } + + @override + Future addUserScripts({required List userScripts}) async { + for (var i = 0; i < userScripts.length; i++) { + await addUserScript(userScript: userScripts[i]); + } + } + + @override + Future removeUserScript({required UserScript userScript}) async { + var index = _userScripts[userScript.injectionTime]?.indexOf(userScript); + if (index == null || index == -1) { + return false; + } + + _userScripts[userScript.injectionTime]?.remove(userScript); + Map args = {}; + args.putIfAbsent('userScript', () => userScript.toMap()); + args.putIfAbsent('index', () => index); + await channel?.invokeMethod('removeUserScript', args); + + return true; + } + + @override + Future removeUserScriptsByGroupName({required String groupName}) async { + final List userScriptsAtDocumentStart = List.from( + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] ?? []); + for (final userScript in userScriptsAtDocumentStart) { + if (userScript.groupName == groupName) { + _userScripts[userScript.injectionTime]?.remove(userScript); + } + } + + final List userScriptsAtDocumentEnd = + List.from(_userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] ?? []); + for (final userScript in userScriptsAtDocumentEnd) { + if (userScript.groupName == groupName) { + _userScripts[userScript.injectionTime]?.remove(userScript); + } + } + + Map args = {}; + args.putIfAbsent('groupName', () => groupName); + await channel?.invokeMethod('removeUserScriptsByGroupName', args); + } + + @override + Future removeUserScripts( + {required List userScripts}) async { + for (final userScript in userScripts) { + await removeUserScript(userScript: userScript); + } + } + + @override + Future removeAllUserScripts() async { + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_START]?.clear(); + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_END]?.clear(); + + Map args = {}; + await channel?.invokeMethod('removeAllUserScripts', args); + } + + @override + bool hasUserScript({required UserScript userScript}) { + return _userScripts[userScript.injectionTime]?.contains(userScript) ?? + false; + } + + @override + Future callAsyncJavaScript( + {required String functionBody, + Map arguments = const {}, + ContentWorld? contentWorld}) async { + Map args = {}; + args.putIfAbsent('functionBody', () => functionBody); + args.putIfAbsent('arguments', () => arguments); + args.putIfAbsent('contentWorld', () => contentWorld?.toMap()); + var data = await channel?.invokeMethod('callAsyncJavaScript', args); + if (data == null) { + return null; + } + data = json.decode(data); + return CallAsyncJavaScriptResult( + value: data["value"], error: data["error"]); + } + + @override + Future saveWebArchive( + {required String filePath, bool autoname = false}) async { + if (!autoname) { + assert(filePath.endsWith("." + WebArchiveFormat.MHT.toNativeValue())); + } + + Map args = {}; + args.putIfAbsent("filePath", () => filePath); + args.putIfAbsent("autoname", () => autoname); + return await channel?.invokeMethod('saveWebArchive', args); + } + + @override + Future isSecureContext() async { + Map args = {}; + return await channel?.invokeMethod('isSecureContext', args) ?? false; + } + + @override + Future createWebMessageChannel() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('createWebMessageChannel', args)) + ?.cast(); + final webMessageChannel = AndroidWebMessageChannel.static().fromMap(result); + if (webMessageChannel != null) { + _webMessageChannels.add(webMessageChannel); + } + return webMessageChannel; + } + + @override + Future postWebMessage( + {required WebMessage message, WebUri? targetOrigin}) async { + if (targetOrigin == null) { + targetOrigin = WebUri(''); + } + Map args = {}; + args.putIfAbsent('message', () => message.toMap()); + args.putIfAbsent('targetOrigin', () => targetOrigin.toString()); + await channel?.invokeMethod('postWebMessage', args); + } + + @override + Future addWebMessageListener( + PlatformWebMessageListener webMessageListener) async { + assert(!_webMessageListeners.contains(webMessageListener), + "${webMessageListener} was already added."); + assert( + !_webMessageListenerObjNames + .contains(webMessageListener.params.jsObjectName), + "jsObjectName ${webMessageListener.params.jsObjectName} was already added."); + _webMessageListeners.add(webMessageListener as AndroidWebMessageListener); + _webMessageListenerObjNames.add(webMessageListener.params.jsObjectName); + + Map args = {}; + args.putIfAbsent('webMessageListener', () => webMessageListener.toMap()); + await channel?.invokeMethod('addWebMessageListener', args); + } + + @override + bool hasWebMessageListener(PlatformWebMessageListener webMessageListener) { + return _webMessageListeners.contains(webMessageListener) || + _webMessageListenerObjNames + .contains(webMessageListener.params.jsObjectName); + } + + @override + Future canScrollVertically() async { + Map args = {}; + return await channel?.invokeMethod('canScrollVertically', args) ?? + false; + } + + @override + Future canScrollHorizontally() async { + Map args = {}; + return await channel?.invokeMethod('canScrollHorizontally', args) ?? + false; + } + + @override + Future startSafeBrowsing() async { + Map args = {}; + return await channel?.invokeMethod('startSafeBrowsing', args) ?? + false; + } + + @override + Future clearSslPreferences() async { + Map args = {}; + await channel?.invokeMethod('clearSslPreferences', args); + } + + @override + Future pause() async { + Map args = {}; + await channel?.invokeMethod('pause', args); + } + + @override + Future resume() async { + Map args = {}; + await channel?.invokeMethod('resume', args); + } + + @override + Future pageDown({required bool bottom}) async { + Map args = {}; + args.putIfAbsent("bottom", () => bottom); + return await channel?.invokeMethod('pageDown', args) ?? false; + } + + @override + Future pageUp({required bool top}) async { + Map args = {}; + args.putIfAbsent("top", () => top); + return await channel?.invokeMethod('pageUp', args) ?? false; + } + + @override + Future zoomIn() async { + Map args = {}; + return await channel?.invokeMethod('zoomIn', args) ?? false; + } + + @override + Future zoomOut() async { + Map args = {}; + return await channel?.invokeMethod('zoomOut', args) ?? false; + } + + @override + Future clearHistory() async { + Map args = {}; + return await channel?.invokeMethod('clearHistory', args); + } + + @override + Future isInFullscreen() async { + Map args = {}; + return await channel?.invokeMethod('isInFullscreen', args) ?? false; + } + + @override + Future clearFormData() async { + Map args = {}; + return await channel?.invokeMethod('clearFormData', args); + } + + @override + Future getDefaultUserAgent() async { + Map args = {}; + return await _staticChannel.invokeMethod( + 'getDefaultUserAgent', args) ?? + ''; + } + + @override + Future clearClientCertPreferences() async { + Map args = {}; + await _staticChannel.invokeMethod('clearClientCertPreferences', args); + } + + @override + Future getSafeBrowsingPrivacyPolicyUrl() async { + Map args = {}; + String? url = await _staticChannel.invokeMethod( + 'getSafeBrowsingPrivacyPolicyUrl', args); + return url != null ? WebUri(url) : null; + } + + @override + @Deprecated("Use setSafeBrowsingAllowlist instead") + Future setSafeBrowsingWhitelist({required List hosts}) async { + return await setSafeBrowsingAllowlist(hosts: hosts); + } + + @override + Future setSafeBrowsingAllowlist({required List hosts}) async { + Map args = {}; + args.putIfAbsent('hosts', () => hosts); + return await _staticChannel.invokeMethod( + 'setSafeBrowsingAllowlist', args) ?? + false; + } + + @override + Future getCurrentWebViewPackage() async { + Map args = {}; + Map? packageInfo = + (await _staticChannel.invokeMethod('getCurrentWebViewPackage', args)) + ?.cast(); + return WebViewPackageInfo.fromMap(packageInfo); + } + + @override + Future setWebContentsDebuggingEnabled(bool debuggingEnabled) async { + Map args = {}; + args.putIfAbsent('debuggingEnabled', () => debuggingEnabled); + return await _staticChannel.invokeMethod( + 'setWebContentsDebuggingEnabled', args); + } + + @override + Future getVariationsHeader() async { + Map args = {}; + return await _staticChannel.invokeMethod( + 'getVariationsHeader', args); + } + + @override + Future isMultiProcessEnabled() async { + Map args = {}; + return await _staticChannel.invokeMethod( + 'isMultiProcessEnabled', args) ?? + false; + } + + @override + Future disableWebView() async { + Map args = {}; + await _staticChannel.invokeMethod('disableWebView', args); + } + + @override + Future disposeKeepAlive(InAppWebViewKeepAlive keepAlive) async { + Map args = {}; + args.putIfAbsent('keepAliveId', () => keepAlive.id); + await _staticChannel.invokeMethod('disposeKeepAlive', args); + _keepAliveMap[keepAlive] = null; + } + + @override + Future clearAllCache({bool includeDiskFiles = true}) async { + Map args = {}; + args.putIfAbsent('includeDiskFiles', () => includeDiskFiles); + await _staticChannel.invokeMethod('clearAllCache', args); + } + + @override + Future get tRexRunnerHtml async => await rootBundle.loadString( + 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.html'); + + @override + Future get tRexRunnerCss async => await rootBundle.loadString( + 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.css'); + + @override + dynamic getViewId() { + return id; + } + + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(removeMethodCallHandler: !isKeepAlive); + _inAppBrowser = null; + webStorage.dispose(); + if (!isKeepAlive) { + _controllerFromPlatform = null; + _javaScriptHandlersMap.clear(); + _userScripts.clear(); + _webMessageListenerObjNames.clear(); + _injectedScriptsFromURL.clear(); + for (final webMessageChannel in _webMessageChannels) { + webMessageChannel.dispose(); + } + _webMessageChannels.clear(); + for (final webMessageListener in _webMessageListeners) { + webMessageListener.dispose(); + } + _webMessageListeners.clear(); + } + } +} + +extension InternalInAppWebViewController on AndroidInAppWebViewController { + get handleMethod => _handleMethod; +} diff --git a/packages/flutter_inappwebview_android/lib/src/in_app_webview/main.dart b/packages/flutter_inappwebview_android/lib/src/in_app_webview/main.dart new file mode 100644 index 0000000..b83b061 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/in_app_webview/main.dart @@ -0,0 +1,3 @@ +export 'in_app_webview_controller.dart' hide InternalInAppWebViewController; +export 'in_app_webview.dart'; +export 'headless_in_app_webview.dart' hide InternalHeadlessInAppWebView; diff --git a/packages/flutter_inappwebview_android/lib/src/inappwebview_platform.dart b/packages/flutter_inappwebview_android/lib/src/inappwebview_platform.dart new file mode 100644 index 0000000..aa5c420 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/inappwebview_platform.dart @@ -0,0 +1,376 @@ +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import 'chrome_safari_browser/chrome_safari_browser.dart'; +import 'cookie_manager.dart'; +import 'http_auth_credentials_database.dart'; +import 'find_interaction/main.dart'; +import 'in_app_browser/in_app_browser.dart'; +import 'in_app_webview/main.dart'; +import 'print_job/main.dart'; +import 'pull_to_refresh/main.dart'; +import 'web_message/main.dart'; +import 'web_storage/main.dart'; +import 'process_global_config.dart'; +import 'proxy_controller.dart'; +import 'service_worker_controller.dart'; +import 'tracing_controller.dart'; +import 'webview_asset_loader.dart'; +import 'webview_feature.dart' as wv; + +/// Implementation of [InAppWebViewPlatform] using the WebView API. +class AndroidInAppWebViewPlatform extends InAppWebViewPlatform { + /// Registers this class as the default instance of [InAppWebViewPlatform]. + static void registerWith() { + InAppWebViewPlatform.instance = AndroidInAppWebViewPlatform(); + } + + /// Creates a new [AndroidCookieManager]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [CookieManager] in `flutter_inappwebview` instead. + @override + AndroidCookieManager createPlatformCookieManager( + PlatformCookieManagerCreationParams params, + ) { + return AndroidCookieManager(params); + } + + /// Creates a new [AndroidInAppWebViewController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebViewController] in `flutter_inappwebview` instead. + @override + AndroidInAppWebViewController createPlatformInAppWebViewController( + PlatformInAppWebViewControllerCreationParams params, + ) { + return AndroidInAppWebViewController(params); + } + + /// Creates a new empty [AndroidInAppWebViewController] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebViewController] in `flutter_inappwebview` instead. + @override + AndroidInAppWebViewController createPlatformInAppWebViewControllerStatic() { + return AndroidInAppWebViewController.static(); + } + + /// Creates a new [AndroidInAppWebViewWidget]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebView] in `flutter_inappwebview` instead. + @override + AndroidInAppWebViewWidget createPlatformInAppWebViewWidget( + PlatformInAppWebViewWidgetCreationParams params, + ) { + return AndroidInAppWebViewWidget(params); + } + + /// Creates a new [AndroidFindInteractionController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [FindInteractionController] in `flutter_inappwebview` instead. + @override + AndroidFindInteractionController createPlatformFindInteractionController( + PlatformFindInteractionControllerCreationParams params, + ) { + return AndroidFindInteractionController(params); + } + + /// Creates a new [AndroidPrintJobController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [PrintJobController] in `flutter_inappwebview` instead. + @override + AndroidPrintJobController createPlatformPrintJobController( + PlatformPrintJobControllerCreationParams params, + ) { + return AndroidPrintJobController(params); + } + + /// Creates a new [AndroidPullToRefreshController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [PullToRefreshController] in `flutter_inappwebview` instead. + @override + AndroidPullToRefreshController createPlatformPullToRefreshController( + PlatformPullToRefreshControllerCreationParams params, + ) { + return AndroidPullToRefreshController(params); + } + + /// Creates a new [AndroidWebMessageChannel]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageChannel] in `flutter_inappwebview` instead. + @override + AndroidWebMessageChannel createPlatformWebMessageChannel( + PlatformWebMessageChannelCreationParams params, + ) { + return AndroidWebMessageChannel(params); + } + + /// Creates a new empty [AndroidWebMessageChannel] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageChannel] in `flutter_inappwebview` instead. + @override + AndroidWebMessageChannel createPlatformWebMessageChannelStatic() { + return AndroidWebMessageChannel.static(); + } + + /// Creates a new [AndroidWebMessageListener]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageListener] in `flutter_inappwebview` instead. + @override + AndroidWebMessageListener createPlatformWebMessageListener( + PlatformWebMessageListenerCreationParams params, + ) { + return AndroidWebMessageListener(params); + } + + /// Creates a new [AndroidJavaScriptReplyProxy]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [JavaScriptReplyProxy] in `flutter_inappwebview` instead. + @override + AndroidJavaScriptReplyProxy createPlatformJavaScriptReplyProxy( + PlatformJavaScriptReplyProxyCreationParams params, + ) { + return AndroidJavaScriptReplyProxy(params); + } + + /// Creates a new [AndroidWebMessagePort]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessagePort] in `flutter_inappwebview` instead. + @override + AndroidWebMessagePort createPlatformWebMessagePort( + PlatformWebMessagePortCreationParams params, + ) { + return AndroidWebMessagePort(params); + } + + /// Creates a new [AndroidWebStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebStorage] in `flutter_inappwebview` instead. + @override + AndroidWebStorage createPlatformWebStorage( + PlatformWebStorageCreationParams params, + ) { + return AndroidWebStorage(params); + } + + /// Creates a new [AndroidLocalStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [LocalStorage] in `flutter_inappwebview` instead. + @override + AndroidLocalStorage createPlatformLocalStorage( + PlatformLocalStorageCreationParams params, + ) { + return AndroidLocalStorage(params); + } + + /// Creates a new [AndroidSessionStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [SessionStorage] in `flutter_inappwebview` instead. + @override + AndroidSessionStorage createPlatformSessionStorage( + PlatformSessionStorageCreationParams params, + ) { + return AndroidSessionStorage(params); + } + + /// Creates a new [AndroidHeadlessInAppWebView]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [HeadlessInAppWebView] in `flutter_inappwebview` instead. + @override + AndroidHeadlessInAppWebView createPlatformHeadlessInAppWebView( + PlatformHeadlessInAppWebViewCreationParams params, + ) { + return AndroidHeadlessInAppWebView(params); + } + + /// Creates a new [AndroidHttpAuthCredentialDatabase]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [HttpAuthCredentialDatabase] in `flutter_inappwebview` instead. + @override + AndroidHttpAuthCredentialDatabase createPlatformHttpAuthCredentialDatabase( + PlatformHttpAuthCredentialDatabaseCreationParams params, + ) { + return AndroidHttpAuthCredentialDatabase(params); + } + + /// Creates a new [AndroidInAppBrowser]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppBrowser] in `flutter_inappwebview` instead. + @override + AndroidInAppBrowser createPlatformInAppBrowser( + PlatformInAppBrowserCreationParams params, + ) { + return AndroidInAppBrowser(params); + } + + /// Creates a new empty [AndroidInAppBrowser] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppBrowser] in `flutter_inappwebview` instead. + @override + AndroidInAppBrowser createPlatformInAppBrowserStatic() { + return AndroidInAppBrowser.static(); + } + + /// Creates a new [AndroidProcessGlobalConfig]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ProcessGlobalConfig] in `flutter_inappwebview` instead. + @override + AndroidProcessGlobalConfig createPlatformProcessGlobalConfig( + PlatformProcessGlobalConfigCreationParams params, + ) { + return AndroidProcessGlobalConfig(params); + } + + /// Creates a new [AndroidProxyController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ProxyController] in `flutter_inappwebview` instead. + @override + AndroidProxyController createPlatformProxyController( + PlatformProxyControllerCreationParams params, + ) { + return AndroidProxyController(params); + } + + /// Creates a new [AndroidServiceWorkerController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ServiceWorkerController] in `flutter_inappwebview` instead. + @override + AndroidServiceWorkerController createPlatformServiceWorkerController( + PlatformServiceWorkerControllerCreationParams params, + ) { + return AndroidServiceWorkerController(params); + } + + /// Creates a new empty [AndroidServiceWorkerController] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ServiceWorkerController] in `flutter_inappwebview` instead. + @override + AndroidServiceWorkerController createPlatformServiceWorkerControllerStatic() { + return AndroidServiceWorkerController.static(); + } + + /// Creates a new [AndroidTracingController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [TracingController] in `flutter_inappwebview` instead. + @override + AndroidTracingController createPlatformTracingController( + PlatformTracingControllerCreationParams params, + ) { + return AndroidTracingController(params); + } + + /// Creates a new [AndroidAssetsPathHandler]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [AssetsPathHandler] in `flutter_inappwebview` instead. + @override + AndroidAssetsPathHandler createPlatformAssetsPathHandler( + PlatformAssetsPathHandlerCreationParams params, + ) { + return AndroidAssetsPathHandler(params); + } + + /// Creates a new [AndroidResourcesPathHandler]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ResourcesPathHandler] in `flutter_inappwebview` instead. + @override + AndroidResourcesPathHandler createPlatformResourcesPathHandler( + PlatformResourcesPathHandlerCreationParams params, + ) { + return AndroidResourcesPathHandler(params); + } + + /// Creates a new [AndroidInternalStoragePathHandler]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InternalStoragePathHandler] in `flutter_inappwebview` instead. + @override + AndroidInternalStoragePathHandler createPlatformInternalStoragePathHandler( + PlatformInternalStoragePathHandlerCreationParams params, + ) { + return AndroidInternalStoragePathHandler(params); + } + + /// Creates a new [AndroidCustomPathHandler]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [CustomPathHandler] in `flutter_inappwebview` instead. + @override + AndroidCustomPathHandler createPlatformCustomPathHandler( + PlatformCustomPathHandlerCreationParams params, + ) { + return AndroidCustomPathHandler(params); + } + + /// Creates a new [wv.AndroidWebViewFeature]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebViewFeature] in `flutter_inappwebview` instead. + @override + wv.AndroidWebViewFeature createPlatformWebViewFeature( + PlatformWebViewFeatureCreationParams params, + ) { + return wv.AndroidWebViewFeature(params); + } + + /// Creates a new empty [wv.AndroidWebViewFeature] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebViewFeature] in `flutter_inappwebview` instead. + @override + wv.AndroidWebViewFeature createPlatformWebViewFeatureStatic() { + return wv.AndroidWebViewFeature.static(); + } + + /// Creates a new [AndroidChromeSafariBrowser]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ChromeSafariBrowser] in `flutter_inappwebview` instead. + @override + AndroidChromeSafariBrowser createPlatformChromeSafariBrowser( + PlatformChromeSafariBrowserCreationParams params, + ) { + return AndroidChromeSafariBrowser(params); + } + + /// Creates a new empty [AndroidChromeSafariBrowser] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ChromeSafariBrowser] in `flutter_inappwebview` instead. + @override + AndroidChromeSafariBrowser createPlatformChromeSafariBrowserStatic() { + return AndroidChromeSafariBrowser.static(); + } + + /// Creates a new empty [AndroidWebStorageManager] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebStorageManager] in `flutter_inappwebview` instead. + @override + AndroidWebStorageManager createPlatformWebStorageManager( + PlatformWebStorageManagerCreationParams params) { + return AndroidWebStorageManager(params); + } +} diff --git a/packages/flutter_inappwebview_android/lib/src/main.dart b/packages/flutter_inappwebview_android/lib/src/main.dart new file mode 100644 index 0000000..b49714a --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/main.dart @@ -0,0 +1,18 @@ +export 'inappwebview_platform.dart'; +export 'in_app_webview/main.dart'; +export 'in_app_browser/main.dart'; +export 'chrome_safari_browser/main.dart'; +export 'web_storage/main.dart'; +export 'cookie_manager.dart' hide InternalCookieManager; +export 'http_auth_credentials_database.dart' + hide InternalHttpAuthCredentialDatabase; +export 'pull_to_refresh/main.dart'; +export 'web_message/main.dart'; +export 'print_job/main.dart'; +export 'find_interaction/main.dart'; +export 'service_worker_controller.dart'; +export 'webview_feature.dart' hide InternalWebViewFeature; +export 'proxy_controller.dart' hide InternalProxyController; +export 'webview_asset_loader.dart'; +export 'tracing_controller.dart' hide InternalTracingController; +export 'process_global_config.dart' hide InternalProcessGlobalConfig; diff --git a/packages/flutter_inappwebview_android/lib/src/platform_util.dart b/packages/flutter_inappwebview_android/lib/src/platform_util.dart new file mode 100644 index 0000000..27e9647 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/platform_util.dart @@ -0,0 +1,64 @@ +import 'package:flutter/services.dart'; + +///Platform native utilities +class PlatformUtil { + static PlatformUtil? _instance; + static const MethodChannel _channel = + MethodChannel('com.pichillilorenzo/flutter_inappwebview_platformutil'); + + PlatformUtil._(); + + ///Get [PlatformUtil] instance. + static PlatformUtil instance() { + return (_instance != null) ? _instance! : _init(); + } + + static PlatformUtil _init() { + _channel.setMethodCallHandler((call) async { + try { + return await _handleMethod(call); + } on Error catch (e) { + print(e); + print(e.stackTrace); + } + }); + _instance = PlatformUtil._(); + return _instance!; + } + + static Future _handleMethod(MethodCall call) async {} + + String? _cachedSystemVersion; + + ///Get current platform system version. + Future getSystemVersion() async { + if (_cachedSystemVersion != null) { + return _cachedSystemVersion!; + } + Map args = {}; + _cachedSystemVersion = + await _channel.invokeMethod('getSystemVersion', args); + return _cachedSystemVersion!; + } + + ///Format date. + Future formatDate( + {required DateTime date, + required String format, + String locale = "en_US", + String timezone = "UTC"}) async { + Map args = {}; + args.putIfAbsent('date', () => date.millisecondsSinceEpoch); + args.putIfAbsent('format', () => format); + args.putIfAbsent('locale', () => locale); + args.putIfAbsent('timezone', () => timezone); + return await _channel.invokeMethod('formatDate', args); + } + + ///Get cookie expiration date used by Web platform. + Future getWebCookieExpirationDate({required DateTime date}) async { + Map args = {}; + args.putIfAbsent('date', () => date.millisecondsSinceEpoch); + return await _channel.invokeMethod('getWebCookieExpirationDate', args); + } +} diff --git a/packages/flutter_inappwebview_android/lib/src/print_job/main.dart b/packages/flutter_inappwebview_android/lib/src/print_job/main.dart new file mode 100644 index 0000000..4e70ad9 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/print_job/main.dart @@ -0,0 +1 @@ +export 'print_job_controller.dart'; diff --git a/packages/flutter_inappwebview_android/lib/src/print_job/print_job_controller.dart b/packages/flutter_inappwebview_android/lib/src/print_job/print_job_controller.dart new file mode 100644 index 0000000..7683115 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/print_job/print_job_controller.dart @@ -0,0 +1,77 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidPrintJobController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformPrintJobControllerCreationParams] for +/// more information. +@immutable +class AndroidPrintJobControllerCreationParams + extends PlatformPrintJobControllerCreationParams { + /// Creates a new [AndroidPrintJobControllerCreationParams] instance. + const AndroidPrintJobControllerCreationParams( + {required super.id, super.onComplete}); + + /// Creates a [AndroidPrintJobControllerCreationParams] instance based on [PlatformPrintJobControllerCreationParams]. + factory AndroidPrintJobControllerCreationParams.fromPlatformPrintJobControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformPrintJobControllerCreationParams params) { + return AndroidPrintJobControllerCreationParams( + id: params.id, onComplete: params.onComplete); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformPrintJobController} +class AndroidPrintJobController extends PlatformPrintJobController + with ChannelController { + /// Constructs a [AndroidPrintJobController]. + AndroidPrintJobController(PlatformPrintJobControllerCreationParams params) + : super.implementation( + params is AndroidPrintJobControllerCreationParams + ? params + : AndroidPrintJobControllerCreationParams + .fromPlatformPrintJobControllerCreationParams(params), + ) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_printjobcontroller_${params.id}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Future cancel() async { + Map args = {}; + await channel?.invokeMethod('cancel', args); + } + + @override + Future restart() async { + Map args = {}; + await channel?.invokeMethod('restart', args); + } + + @override + Future getInfo() async { + Map args = {}; + Map? infoMap = + (await channel?.invokeMethod('getInfo', args))?.cast(); + return PrintJobInfo.fromMap(infoMap); + } + + @override + Future dispose() async { + Map args = {}; + await channel?.invokeMethod('dispose', args); + disposeChannel(); + } +} diff --git a/packages/flutter_inappwebview_android/lib/src/process_global_config.dart b/packages/flutter_inappwebview_android/lib/src/process_global_config.dart new file mode 100644 index 0000000..1f4d6fe --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/process_global_config.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidProcessGlobalConfig]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformProcessGlobalConfigCreationParams] for +/// more information. +@immutable +class AndroidProcessGlobalConfigCreationParams + extends PlatformProcessGlobalConfigCreationParams { + /// Creates a new [AndroidProcessGlobalConfigCreationParams] instance. + const AndroidProcessGlobalConfigCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformProcessGlobalConfigCreationParams params, + ) : super(); + + /// Creates a [AndroidProcessGlobalConfigCreationParams] instance based on [PlatformProcessGlobalConfigCreationParams]. + factory AndroidProcessGlobalConfigCreationParams.fromPlatformProcessGlobalConfigCreationParams( + PlatformProcessGlobalConfigCreationParams params) { + return AndroidProcessGlobalConfigCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformProcessGlobalConfig} +class AndroidProcessGlobalConfig extends PlatformProcessGlobalConfig + with ChannelController { + /// Creates a new [AndroidProcessGlobalConfig]. + AndroidProcessGlobalConfig(PlatformProcessGlobalConfigCreationParams params) + : super.implementation( + params is AndroidProcessGlobalConfigCreationParams + ? params + : AndroidProcessGlobalConfigCreationParams + .fromPlatformProcessGlobalConfigCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_processglobalconfig'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidProcessGlobalConfig? _instance; + + ///Gets the [AndroidProcessGlobalConfig] shared instance. + static AndroidProcessGlobalConfig instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidProcessGlobalConfig _init() { + _instance = AndroidProcessGlobalConfig( + AndroidProcessGlobalConfigCreationParams( + const PlatformProcessGlobalConfigCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future apply({required ProcessGlobalConfigSettings settings}) async { + Map args = {}; + args.putIfAbsent("settings", () => settings.toMap()); + await channel?.invokeMethod('apply', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalProcessGlobalConfig on AndroidProcessGlobalConfig { + get handleMethod => _handleMethod; +} diff --git a/packages/flutter_inappwebview_android/lib/src/proxy_controller.dart b/packages/flutter_inappwebview_android/lib/src/proxy_controller.dart new file mode 100644 index 0000000..07649ac --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/proxy_controller.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidProxyController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformProxyControllerCreationParams] for +/// more information. +@immutable +class AndroidProxyControllerCreationParams + extends PlatformProxyControllerCreationParams { + /// Creates a new [AndroidProxyControllerCreationParams] instance. + const AndroidProxyControllerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformProxyControllerCreationParams params, + ) : super(); + + /// Creates a [AndroidProxyControllerCreationParams] instance based on [PlatformProxyControllerCreationParams]. + factory AndroidProxyControllerCreationParams.fromPlatformProxyControllerCreationParams( + PlatformProxyControllerCreationParams params) { + return AndroidProxyControllerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformProxyController} +class AndroidProxyController extends PlatformProxyController + with ChannelController { + /// Creates a new [AndroidProxyController]. + AndroidProxyController(PlatformProxyControllerCreationParams params) + : super.implementation( + params is AndroidProxyControllerCreationParams + ? params + : AndroidProxyControllerCreationParams + .fromPlatformProxyControllerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_proxycontroller'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidProxyController? _instance; + + ///Gets the [AndroidProxyController] shared instance. + static AndroidProxyController instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidProxyController _init() { + _instance = AndroidProxyController(AndroidProxyControllerCreationParams( + const PlatformProxyControllerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future setProxyOverride({required ProxySettings settings}) async { + Map args = {}; + args.putIfAbsent("settings", () => settings.toMap()); + await channel?.invokeMethod('setProxyOverride', args); + } + + @override + Future clearProxyOverride() async { + Map args = {}; + await channel?.invokeMethod('clearProxyOverride', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalProxyController on AndroidProxyController { + get handleMethod => _handleMethod; +} diff --git a/packages/flutter_inappwebview_android/lib/src/pull_to_refresh/main.dart b/packages/flutter_inappwebview_android/lib/src/pull_to_refresh/main.dart new file mode 100644 index 0000000..586af9d --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/pull_to_refresh/main.dart @@ -0,0 +1 @@ +export 'pull_to_refresh_controller.dart' hide InternalPullToRefreshController; diff --git a/packages/flutter_inappwebview_android/lib/src/pull_to_refresh/pull_to_refresh_controller.dart b/packages/flutter_inappwebview_android/lib/src/pull_to_refresh/pull_to_refresh_controller.dart new file mode 100644 index 0000000..693c98a --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/pull_to_refresh/pull_to_refresh_controller.dart @@ -0,0 +1,163 @@ +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidPullToRefreshController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformPullToRefreshControllerCreationParams] for +/// more information. +class AndroidPullToRefreshControllerCreationParams + extends PlatformPullToRefreshControllerCreationParams { + /// Creates a new [AndroidPullToRefreshControllerCreationParams] instance. + AndroidPullToRefreshControllerCreationParams( + {super.onRefresh, super.options, super.settings}); + + /// Creates a [AndroidPullToRefreshControllerCreationParams] instance based on [PlatformPullToRefreshControllerCreationParams]. + factory AndroidPullToRefreshControllerCreationParams.fromPlatformPullToRefreshControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformPullToRefreshControllerCreationParams params) { + return AndroidPullToRefreshControllerCreationParams( + onRefresh: params.onRefresh, + options: params.options, + settings: params.settings); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformPullToRefreshController} +class AndroidPullToRefreshController extends PlatformPullToRefreshController + with ChannelController { + /// Constructs a [AndroidPullToRefreshController]. + AndroidPullToRefreshController( + PlatformPullToRefreshControllerCreationParams params) + : super.implementation( + params is AndroidPullToRefreshControllerCreationParams + ? params + : AndroidPullToRefreshControllerCreationParams + .fromPlatformPullToRefreshControllerCreationParams(params), + ); + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + debugLoggingSettings: + PlatformPullToRefreshController.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onRefresh": + if (params.onRefresh != null) params.onRefresh!(); + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + Future setEnabled(bool enabled) async { + Map args = {}; + args.putIfAbsent('enabled', () => enabled); + await channel?.invokeMethod('setEnabled', args); + } + + @override + Future isEnabled() async { + Map args = {}; + return await channel?.invokeMethod('isEnabled', args) ?? false; + } + + Future _setRefreshing(bool refreshing) async { + Map args = {}; + args.putIfAbsent('refreshing', () => refreshing); + await channel?.invokeMethod('setRefreshing', args); + } + + @override + Future beginRefreshing() async { + return await _setRefreshing(true); + } + + @override + Future endRefreshing() async { + await _setRefreshing(false); + } + + @override + Future isRefreshing() async { + Map args = {}; + return await channel?.invokeMethod('isRefreshing', args) ?? false; + } + + @override + Future setColor(Color color) async { + Map args = {}; + args.putIfAbsent('color', () => color.toHex()); + await channel?.invokeMethod('setColor', args); + } + + @override + Future setBackgroundColor(Color color) async { + Map args = {}; + args.putIfAbsent('color', () => color.toHex()); + await channel?.invokeMethod('setBackgroundColor', args); + } + + @override + Future setDistanceToTriggerSync(int distanceToTriggerSync) async { + Map args = {}; + args.putIfAbsent('distanceToTriggerSync', () => distanceToTriggerSync); + await channel?.invokeMethod('setDistanceToTriggerSync', args); + } + + @override + Future setSlingshotDistance(int slingshotDistance) async { + Map args = {}; + args.putIfAbsent('slingshotDistance', () => slingshotDistance); + await channel?.invokeMethod('setSlingshotDistance', args); + } + + @override + Future getDefaultSlingshotDistance() async { + Map args = {}; + return await channel?.invokeMethod( + 'getDefaultSlingshotDistance', args) ?? + 0; + } + + @Deprecated("Use setIndicatorSize instead") + @override + Future setSize(AndroidPullToRefreshSize size) async { + Map args = {}; + args.putIfAbsent('size', () => size.toNativeValue()); + await channel?.invokeMethod('setSize', args); + } + + @override + Future setIndicatorSize(PullToRefreshSize size) async { + Map args = {}; + args.putIfAbsent('size', () => size.toNativeValue()); + await channel?.invokeMethod('setSize', args); + } + + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(removeMethodCallHandler: !isKeepAlive); + } +} + +extension InternalPullToRefreshController on AndroidPullToRefreshController { + void init(dynamic id) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } +} diff --git a/packages/flutter_inappwebview_android/lib/src/service_worker_controller.dart b/packages/flutter_inappwebview_android/lib/src/service_worker_controller.dart new file mode 100644 index 0000000..646b958 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/service_worker_controller.dart @@ -0,0 +1,161 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidServiceWorkerController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformServiceWorkerControllerCreationParams] for +/// more information. +@immutable +class AndroidServiceWorkerControllerCreationParams + extends PlatformServiceWorkerControllerCreationParams { + /// Creates a new [AndroidServiceWorkerControllerCreationParams] instance. + const AndroidServiceWorkerControllerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformServiceWorkerControllerCreationParams params, + ) : super(); + + /// Creates a [AndroidServiceWorkerControllerCreationParams] instance based on [PlatformServiceWorkerControllerCreationParams]. + factory AndroidServiceWorkerControllerCreationParams.fromPlatformServiceWorkerControllerCreationParams( + PlatformServiceWorkerControllerCreationParams params) { + return AndroidServiceWorkerControllerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformServiceWorkerController} +class AndroidServiceWorkerController extends PlatformServiceWorkerController + with ChannelController { + /// Creates a new [AndroidServiceWorkerController]. + AndroidServiceWorkerController( + PlatformServiceWorkerControllerCreationParams params) + : super.implementation( + params is AndroidServiceWorkerControllerCreationParams + ? params + : AndroidServiceWorkerControllerCreationParams + .fromPlatformServiceWorkerControllerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_serviceworkercontroller'); + handler = handleMethod; + initMethodCallHandler(); + } + + factory AndroidServiceWorkerController.static() { + return instance(); + } + + static AndroidServiceWorkerController? _instance; + + ///Gets the [AndroidServiceWorkerController] shared instance. + static AndroidServiceWorkerController instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidServiceWorkerController _init() { + _instance = AndroidServiceWorkerController( + AndroidServiceWorkerControllerCreationParams( + const PlatformServiceWorkerControllerCreationParams())); + return _instance!; + } + + ServiceWorkerClient? _serviceWorkerClient; + + @override + ServiceWorkerClient? get serviceWorkerClient => _serviceWorkerClient; + + @override + Future setServiceWorkerClient(ServiceWorkerClient? value) async { + Map args = {}; + args.putIfAbsent('isNull', () => value == null); + await channel?.invokeMethod("setServiceWorkerClient", args); + _serviceWorkerClient = value; + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "shouldInterceptRequest": + if (serviceWorkerClient != null && + serviceWorkerClient!.shouldInterceptRequest != null) { + Map arguments = + call.arguments.cast(); + WebResourceRequest request = WebResourceRequest.fromMap(arguments)!; + + return (await serviceWorkerClient!.shouldInterceptRequest!(request)) + ?.toMap(); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + + return null; + } + + @override + Future getAllowContentAccess() async { + Map args = {}; + return await channel?.invokeMethod('getAllowContentAccess', args) ?? + false; + } + + @override + Future getAllowFileAccess() async { + Map args = {}; + return await channel?.invokeMethod('getAllowFileAccess', args) ?? + false; + } + + @override + Future getBlockNetworkLoads() async { + Map args = {}; + return await channel?.invokeMethod('getBlockNetworkLoads', args) ?? + false; + } + + @override + Future getCacheMode() async { + Map args = {}; + return CacheMode.fromNativeValue( + await channel?.invokeMethod('getCacheMode', args)); + } + + @override + Future setAllowContentAccess(bool allow) async { + Map args = {}; + args.putIfAbsent("allow", () => allow); + await channel?.invokeMethod('setAllowContentAccess', args); + } + + @override + Future setAllowFileAccess(bool allow) async { + Map args = {}; + args.putIfAbsent("allow", () => allow); + await channel?.invokeMethod('setAllowFileAccess', args); + } + + @override + Future setBlockNetworkLoads(bool flag) async { + Map args = {}; + args.putIfAbsent("flag", () => flag); + await channel?.invokeMethod('setBlockNetworkLoads', args); + } + + @override + Future setCacheMode(CacheMode mode) async { + Map args = {}; + args.putIfAbsent("mode", () => mode.toNativeValue()); + await channel?.invokeMethod('setCacheMode', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalServiceWorkerController on AndroidServiceWorkerController { + get handleMethod => _handleMethod; +} diff --git a/packages/flutter_inappwebview_android/lib/src/tracing_controller.dart b/packages/flutter_inappwebview_android/lib/src/tracing_controller.dart new file mode 100644 index 0000000..9d619f0 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/tracing_controller.dart @@ -0,0 +1,88 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidTracingController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformTracingControllerCreationParams] for +/// more information. +@immutable +class AndroidTracingControllerCreationParams + extends PlatformTracingControllerCreationParams { + /// Creates a new [AndroidTracingControllerCreationParams] instance. + const AndroidTracingControllerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformTracingControllerCreationParams params, + ) : super(); + + /// Creates a [AndroidTracingControllerCreationParams] instance based on [PlatformTracingControllerCreationParams]. + factory AndroidTracingControllerCreationParams.fromPlatformTracingControllerCreationParams( + PlatformTracingControllerCreationParams params) { + return AndroidTracingControllerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformTracingController} +class AndroidTracingController extends PlatformTracingController + with ChannelController { + /// Creates a new [AndroidTracingController]. + AndroidTracingController(PlatformTracingControllerCreationParams params) + : super.implementation( + params is AndroidTracingControllerCreationParams + ? params + : AndroidTracingControllerCreationParams + .fromPlatformTracingControllerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_tracingcontroller'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidTracingController? _instance; + + ///Gets the [AndroidTracingController] shared instance. + static AndroidTracingController instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidTracingController _init() { + _instance = AndroidTracingController(AndroidTracingControllerCreationParams( + const PlatformTracingControllerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future start({required TracingSettings settings}) async { + Map args = {}; + args.putIfAbsent("settings", () => settings.toMap()); + await channel?.invokeMethod('start', args); + } + + @override + Future stop({String? filePath}) async { + Map args = {}; + args.putIfAbsent("filePath", () => filePath); + return await channel?.invokeMethod('stop', args) ?? false; + } + + @override + Future isTracing() async { + Map args = {}; + return await channel?.invokeMethod('isTracing', args) ?? false; + } + + @override + void dispose() { + // empty + } +} + +extension InternalTracingController on AndroidTracingController { + get handleMethod => _handleMethod; +} diff --git a/packages/flutter_inappwebview_android/lib/src/web_message/main.dart b/packages/flutter_inappwebview_android/lib/src/web_message/main.dart new file mode 100644 index 0000000..d41e30c --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/web_message/main.dart @@ -0,0 +1,3 @@ +export 'web_message_port.dart' hide InternalWebMessagePort; +export 'web_message_channel.dart' hide InternalWebMessageChannel; +export 'web_message_listener.dart'; diff --git a/packages/flutter_inappwebview_android/lib/src/web_message/web_message_channel.dart b/packages/flutter_inappwebview_android/lib/src/web_message/web_message_channel.dart new file mode 100644 index 0000000..9af8730 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/web_message/web_message_channel.dart @@ -0,0 +1,120 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import 'web_message_port.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebMessageChannel]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessageChannelCreationParams] for +/// more information. +@immutable +class AndroidWebMessageChannelCreationParams + extends PlatformWebMessageChannelCreationParams { + /// Creates a new [AndroidWebMessageChannelCreationParams] instance. + const AndroidWebMessageChannelCreationParams( + {required super.id, required super.port1, required super.port2}); + + /// Creates a [AndroidWebMessageChannelCreationParams] instance based on [PlatformWebMessageChannelCreationParams]. + factory AndroidWebMessageChannelCreationParams.fromPlatformWebMessageChannelCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessageChannelCreationParams params) { + return AndroidWebMessageChannelCreationParams( + id: params.id, port1: params.port1, port2: params.port2); + } + + @override + String toString() { + return 'AndroidWebMessageChannelCreationParams{id: $id, port1: $port1, port2: $port2}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessageChannel} +class AndroidWebMessageChannel extends PlatformWebMessageChannel + with ChannelController { + /// Constructs a [AndroidWebMessageChannel]. + AndroidWebMessageChannel(PlatformWebMessageChannelCreationParams params) + : super.implementation( + params is AndroidWebMessageChannelCreationParams + ? params + : AndroidWebMessageChannelCreationParams + .fromPlatformWebMessageChannelCreationParams(params), + ) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_web_message_channel_${params.id}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + static final AndroidWebMessageChannel _staticValue = AndroidWebMessageChannel( + AndroidWebMessageChannelCreationParams( + id: '', + port1: AndroidWebMessagePort( + AndroidWebMessagePortCreationParams(index: 0)), + port2: AndroidWebMessagePort( + AndroidWebMessagePortCreationParams(index: 1)))); + + /// Provide static access. + factory AndroidWebMessageChannel.static() { + return _staticValue; + } + + AndroidWebMessagePort get _androidPort1 => port1 as AndroidWebMessagePort; + + AndroidWebMessagePort get _androidPort2 => port2 as AndroidWebMessagePort; + + static AndroidWebMessageChannel? _fromMap(Map? map) { + if (map == null) { + return null; + } + var webMessageChannel = AndroidWebMessageChannel( + AndroidWebMessageChannelCreationParams( + id: map["id"], + port1: AndroidWebMessagePort( + AndroidWebMessagePortCreationParams(index: 0)), + port2: AndroidWebMessagePort( + AndroidWebMessagePortCreationParams(index: 1)))); + webMessageChannel._androidPort1.webMessageChannel = webMessageChannel; + webMessageChannel._androidPort2.webMessageChannel = webMessageChannel; + return webMessageChannel; + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onMessage": + int index = call.arguments["index"]; + var port = index == 0 ? _androidPort1 : _androidPort2; + if (port.onMessage != null) { + WebMessage? message = call.arguments["message"] != null + ? WebMessage.fromMap( + call.arguments["message"].cast()) + : null; + port.onMessage!(message); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + AndroidWebMessageChannel? fromMap(Map? map) { + return _fromMap(map); + } + + @override + void dispose() { + disposeChannel(); + } + + @override + String toString() { + return 'AndroidWebMessageChannel{id: $id, port1: $port1, port2: $port2}'; + } +} + +extension InternalWebMessageChannel on AndroidWebMessageChannel { + MethodChannel? get internalChannel => channel; +} diff --git a/packages/flutter_inappwebview_android/lib/src/web_message/web_message_listener.dart b/packages/flutter_inappwebview_android/lib/src/web_message/web_message_listener.dart new file mode 100644 index 0000000..00307c9 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/web_message/web_message_listener.dart @@ -0,0 +1,164 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebMessageListener]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessageListenerCreationParams] for +/// more information. +@immutable +class AndroidWebMessageListenerCreationParams + extends PlatformWebMessageListenerCreationParams { + /// Creates a new [AndroidWebMessageListenerCreationParams] instance. + const AndroidWebMessageListenerCreationParams( + {required this.allowedOriginRules, + required super.jsObjectName, + super.onPostMessage}); + + /// Creates a [AndroidWebMessageListenerCreationParams] instance based on [PlatformWebMessageListenerCreationParams]. + factory AndroidWebMessageListenerCreationParams.fromPlatformWebMessageListenerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessageListenerCreationParams params) { + return AndroidWebMessageListenerCreationParams( + allowedOriginRules: params.allowedOriginRules ?? Set.from(["*"]), + jsObjectName: params.jsObjectName, + onPostMessage: params.onPostMessage); + } + + @override + final Set allowedOriginRules; + + @override + String toString() { + return 'AndroidWebMessageListenerCreationParams{jsObjectName: $jsObjectName, allowedOriginRules: $allowedOriginRules, onPostMessage: $onPostMessage}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessageListener} +class AndroidWebMessageListener extends PlatformWebMessageListener + with ChannelController { + /// Constructs a [AndroidWebMessageListener]. + AndroidWebMessageListener(PlatformWebMessageListenerCreationParams params) + : super.implementation( + params is AndroidWebMessageListenerCreationParams + ? params + : AndroidWebMessageListenerCreationParams + .fromPlatformWebMessageListenerCreationParams(params), + ) { + assert(!this._androidParams.allowedOriginRules.contains(""), + "allowedOriginRules cannot contain empty strings"); + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_web_message_listener_${_id}_${params.jsObjectName}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + ///Message Listener ID used internally. + final String _id = IdGenerator.generate(); + + AndroidJavaScriptReplyProxy? _replyProxy; + + AndroidWebMessageListenerCreationParams get _androidParams => + params as AndroidWebMessageListenerCreationParams; + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onPostMessage": + if (_replyProxy == null) { + _replyProxy = AndroidJavaScriptReplyProxy( + PlatformJavaScriptReplyProxyCreationParams( + webMessageListener: this)); + } + if (onPostMessage != null) { + WebMessage? message = call.arguments["message"] != null + ? WebMessage.fromMap( + call.arguments["message"].cast()) + : null; + WebUri? sourceOrigin = call.arguments["sourceOrigin"] != null + ? WebUri(call.arguments["sourceOrigin"]) + : null; + bool isMainFrame = call.arguments["isMainFrame"]; + onPostMessage!(message, sourceOrigin, isMainFrame, _replyProxy!); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + void dispose() { + disposeChannel(); + } + + @override + Map toMap() { + return { + "id": _id, + "jsObjectName": params.jsObjectName, + "allowedOriginRules": _androidParams.allowedOriginRules.toList(), + }; + } + + @override + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return 'AndroidWebMessageListener{id: ${_id}, jsObjectName: ${params.jsObjectName}, allowedOriginRules: ${params.allowedOriginRules}, replyProxy: $_replyProxy}'; + } +} + +/// Object specifying creation parameters for creating a [AndroidJavaScriptReplyProxy]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformJavaScriptReplyProxyCreationParams] for +/// more information. +@immutable +class AndroidJavaScriptReplyProxyCreationParams + extends PlatformJavaScriptReplyProxyCreationParams { + /// Creates a new [AndroidJavaScriptReplyProxyCreationParams] instance. + const AndroidJavaScriptReplyProxyCreationParams( + {required super.webMessageListener}); + + /// Creates a [AndroidJavaScriptReplyProxyCreationParams] instance based on [PlatformJavaScriptReplyProxyCreationParams]. + factory AndroidJavaScriptReplyProxyCreationParams.fromPlatformJavaScriptReplyProxyCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformJavaScriptReplyProxyCreationParams params) { + return AndroidJavaScriptReplyProxyCreationParams( + webMessageListener: params.webMessageListener); + } +} + +///{@macro flutter_inappwebview_platform_interface.JavaScriptReplyProxy} +class AndroidJavaScriptReplyProxy extends PlatformJavaScriptReplyProxy { + /// Constructs a [AndroidWebMessageListener]. + AndroidJavaScriptReplyProxy(PlatformJavaScriptReplyProxyCreationParams params) + : super.implementation( + params is AndroidJavaScriptReplyProxyCreationParams + ? params + : AndroidJavaScriptReplyProxyCreationParams + .fromPlatformJavaScriptReplyProxyCreationParams(params), + ); + + AndroidWebMessageListener get _androidWebMessageListener => + params.webMessageListener as AndroidWebMessageListener; + + @override + Future postMessage(WebMessage message) async { + Map args = {}; + args.putIfAbsent('message', () => message.toMap()); + await _androidWebMessageListener.channel?.invokeMethod('postMessage', args); + } + + @override + String toString() { + return 'AndroidJavaScriptReplyProxy{}'; + } +} diff --git a/packages/flutter_inappwebview_android/lib/src/web_message/web_message_port.dart b/packages/flutter_inappwebview_android/lib/src/web_message/web_message_port.dart new file mode 100644 index 0000000..f8979f5 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/web_message/web_message_port.dart @@ -0,0 +1,95 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import 'web_message_channel.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebMessagePort]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessagePortCreationParams] for +/// more information. +@immutable +class AndroidWebMessagePortCreationParams + extends PlatformWebMessagePortCreationParams { + /// Creates a new [AndroidWebMessagePortCreationParams] instance. + const AndroidWebMessagePortCreationParams({required super.index}); + + /// Creates a [AndroidWebMessagePortCreationParams] instance based on [PlatformWebMessagePortCreationParams]. + factory AndroidWebMessagePortCreationParams.fromPlatformWebMessagePortCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessagePortCreationParams params) { + return AndroidWebMessagePortCreationParams(index: params.index); + } + + @override + String toString() { + return 'AndroidWebMessagePortCreationParams{index: $index}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessagePort} +class AndroidWebMessagePort extends PlatformWebMessagePort { + WebMessageCallback? _onMessage; + late AndroidWebMessageChannel _webMessageChannel; + + /// Constructs a [AndroidWebMessagePort]. + AndroidWebMessagePort(PlatformWebMessagePortCreationParams params) + : super.implementation( + params is AndroidWebMessagePortCreationParams + ? params + : AndroidWebMessagePortCreationParams + .fromPlatformWebMessagePortCreationParams(params), + ); + + @override + Future setWebMessageCallback(WebMessageCallback? onMessage) async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + await _webMessageChannel.internalChannel + ?.invokeMethod('setWebMessageCallback', args); + this._onMessage = onMessage; + } + + @override + Future postMessage(WebMessage message) async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + args.putIfAbsent('message', () => message.toMap()); + await _webMessageChannel.internalChannel?.invokeMethod('postMessage', args); + } + + @override + Future close() async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + await _webMessageChannel.internalChannel?.invokeMethod('close', args); + } + + @override + Map toMap() { + return { + "index": params.index, + "webMessageChannelId": this._webMessageChannel.params.id + }; + } + + @override + Map toJson() { + return toMap(); + } + + @override + String toString() { + return 'AndroidWebMessagePort{index: ${params.index}}'; + } +} + +extension InternalWebMessagePort on AndroidWebMessagePort { + WebMessageCallback? get onMessage => _onMessage; + void set onMessage(WebMessageCallback? value) => _onMessage = value; + + AndroidWebMessageChannel get webMessageChannel => _webMessageChannel; + void set webMessageChannel(AndroidWebMessageChannel value) => + _webMessageChannel = value; +} diff --git a/packages/flutter_inappwebview_android/lib/src/web_storage/main.dart b/packages/flutter_inappwebview_android/lib/src/web_storage/main.dart new file mode 100644 index 0000000..7265ae5 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/web_storage/main.dart @@ -0,0 +1,2 @@ +export 'web_storage.dart'; +export 'web_storage_manager.dart' hide InternalWebStorageManager; diff --git a/packages/flutter_inappwebview_android/lib/src/web_storage/web_storage.dart b/packages/flutter_inappwebview_android/lib/src/web_storage/web_storage.dart new file mode 100644 index 0000000..ad2a300 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/web_storage/web_storage.dart @@ -0,0 +1,257 @@ +import 'dart:convert'; + +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../in_app_webview/in_app_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebStorageCreationParams] for +/// more information. +class AndroidWebStorageCreationParams extends PlatformWebStorageCreationParams { + /// Creates a new [AndroidWebStorageCreationParams] instance. + AndroidWebStorageCreationParams( + {required super.localStorage, required super.sessionStorage}); + + /// Creates a [AndroidWebStorageCreationParams] instance based on [PlatformWebStorageCreationParams]. + factory AndroidWebStorageCreationParams.fromPlatformWebStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebStorageCreationParams params) { + return AndroidWebStorageCreationParams( + localStorage: params.localStorage, + sessionStorage: params.sessionStorage); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebStorage} +class AndroidWebStorage extends PlatformWebStorage { + /// Constructs a [AndroidWebStorage]. + AndroidWebStorage(PlatformWebStorageCreationParams params) + : super.implementation( + params is AndroidWebStorageCreationParams + ? params + : AndroidWebStorageCreationParams + .fromPlatformWebStorageCreationParams(params), + ); + + @override + PlatformLocalStorage get localStorage => params.localStorage; + + @override + PlatformSessionStorage get sessionStorage => params.sessionStorage; + + @override + void dispose() { + localStorage.dispose(); + sessionStorage.dispose(); + } +} + +/// Object specifying creation parameters for creating a [AndroidStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformStorageCreationParams] for +/// more information. +class AndroidStorageCreationParams extends PlatformStorageCreationParams { + /// Creates a new [AndroidStorageCreationParams] instance. + AndroidStorageCreationParams( + {required super.controller, required super.webStorageType}); + + /// Creates a [AndroidStorageCreationParams] instance based on [PlatformStorageCreationParams]. + factory AndroidStorageCreationParams.fromPlatformStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformStorageCreationParams params) { + return AndroidStorageCreationParams( + controller: params.controller, webStorageType: params.webStorageType); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformStorage} +abstract mixin class AndroidStorage implements PlatformStorage { + @override + AndroidInAppWebViewController? controller; + + @override + Future length() async { + var result = await controller?.evaluateJavascript(source: """ + window.$webStorageType.length; + """); + return result != null ? int.parse(json.decode(result)) : null; + } + + @override + Future setItem({required String key, required dynamic value}) async { + var encodedValue = json.encode(value); + await controller?.evaluateJavascript(source: """ + window.$webStorageType.setItem("$key", ${value is String ? encodedValue : "JSON.stringify($encodedValue)"}); + """); + } + + @override + Future getItem({required String key}) async { + var itemValue = await controller?.evaluateJavascript(source: """ + window.$webStorageType.getItem("$key"); + """); + + if (itemValue == null) { + return null; + } + + try { + return json.decode(itemValue); + } catch (e) {} + + return itemValue; + } + + @override + Future removeItem({required String key}) async { + await controller?.evaluateJavascript(source: """ + window.$webStorageType.removeItem("$key"); + """); + } + + @override + Future> getItems() async { + var webStorageItems = []; + + List>? items = + (await controller?.evaluateJavascript(source: """ +(function() { + var webStorageItems = []; + for(var i = 0; i < window.$webStorageType.length; i++){ + var key = window.$webStorageType.key(i); + webStorageItems.push( + { + key: key, + value: window.$webStorageType.getItem(key) + } + ); + } + return webStorageItems; +})(); + """))?.cast>(); + + if (items == null) { + return webStorageItems; + } + + for (var item in items) { + webStorageItems + .add(WebStorageItem(key: item["key"], value: item["value"])); + } + + return webStorageItems; + } + + @override + Future clear() async { + await controller?.evaluateJavascript(source: """ + window.$webStorageType.clear(); + """); + } + + @override + Future key({required int index}) async { + var result = await controller?.evaluateJavascript(source: """ + window.$webStorageType.key($index); + """); + return result != null ? json.decode(result) : null; + } + + @override + void dispose() { + controller = null; + } +} + +/// Object specifying creation parameters for creating a [AndroidLocalStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformLocalStorageCreationParams] for +/// more information. +class AndroidLocalStorageCreationParams + extends PlatformLocalStorageCreationParams { + /// Creates a new [AndroidLocalStorageCreationParams] instance. + AndroidLocalStorageCreationParams(super.params); + + /// Creates a [AndroidLocalStorageCreationParams] instance based on [PlatformLocalStorageCreationParams]. + factory AndroidLocalStorageCreationParams.fromPlatformLocalStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformLocalStorageCreationParams params) { + return AndroidLocalStorageCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformLocalStorage} +class AndroidLocalStorage extends PlatformLocalStorage with AndroidStorage { + /// Constructs a [AndroidLocalStorage]. + AndroidLocalStorage(PlatformLocalStorageCreationParams params) + : super.implementation( + params is AndroidLocalStorageCreationParams + ? params + : AndroidLocalStorageCreationParams + .fromPlatformLocalStorageCreationParams(params), + ); + + /// Default storage + factory AndroidLocalStorage.defaultStorage( + {required PlatformInAppWebViewController? controller}) { + return AndroidLocalStorage(AndroidLocalStorageCreationParams( + PlatformLocalStorageCreationParams(PlatformStorageCreationParams( + controller: controller, + webStorageType: WebStorageType.LOCAL_STORAGE)))); + } + + @override + AndroidInAppWebViewController? get controller => + params.controller as AndroidInAppWebViewController?; +} + +/// Object specifying creation parameters for creating a [AndroidSessionStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformSessionStorageCreationParams] for +/// more information. +class AndroidSessionStorageCreationParams + extends PlatformSessionStorageCreationParams { + /// Creates a new [AndroidSessionStorageCreationParams] instance. + AndroidSessionStorageCreationParams(super.params); + + /// Creates a [AndroidSessionStorageCreationParams] instance based on [PlatformSessionStorageCreationParams]. + factory AndroidSessionStorageCreationParams.fromPlatformSessionStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformSessionStorageCreationParams params) { + return AndroidSessionStorageCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformSessionStorage} +class AndroidSessionStorage extends PlatformSessionStorage with AndroidStorage { + /// Constructs a [AndroidSessionStorage]. + AndroidSessionStorage(PlatformSessionStorageCreationParams params) + : super.implementation( + params is AndroidSessionStorageCreationParams + ? params + : AndroidSessionStorageCreationParams + .fromPlatformSessionStorageCreationParams(params), + ); + + /// Default storage + factory AndroidSessionStorage.defaultStorage( + {required PlatformInAppWebViewController? controller}) { + return AndroidSessionStorage(AndroidSessionStorageCreationParams( + PlatformSessionStorageCreationParams(PlatformStorageCreationParams( + controller: controller, + webStorageType: WebStorageType.SESSION_STORAGE)))); + } + + @override + AndroidInAppWebViewController? get controller => + params.controller as AndroidInAppWebViewController?; +} diff --git a/packages/flutter_inappwebview_android/lib/src/web_storage/web_storage_manager.dart b/packages/flutter_inappwebview_android/lib/src/web_storage/web_storage_manager.dart new file mode 100644 index 0000000..d5fc456 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/web_storage/web_storage_manager.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebStorageManager]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebStorageManagerCreationParams] for +/// more information. +@immutable +class AndroidWebStorageManagerCreationParams + extends PlatformWebStorageManagerCreationParams { + /// Creates a new [AndroidWebStorageManagerCreationParams] instance. + const AndroidWebStorageManagerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformWebStorageManagerCreationParams params, + ) : super(); + + /// Creates a [AndroidWebStorageManagerCreationParams] instance based on [PlatformWebStorageManagerCreationParams]. + factory AndroidWebStorageManagerCreationParams.fromPlatformWebStorageManagerCreationParams( + PlatformWebStorageManagerCreationParams params) { + return AndroidWebStorageManagerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebStorageManager} +class AndroidWebStorageManager extends PlatformWebStorageManager + with ChannelController { + /// Creates a new [AndroidWebStorageManager]. + AndroidWebStorageManager(PlatformWebStorageManagerCreationParams params) + : super.implementation( + params is AndroidWebStorageManagerCreationParams + ? params + : AndroidWebStorageManagerCreationParams + .fromPlatformWebStorageManagerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_webstoragemanager'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidWebStorageManager? _instance; + + ///Gets the WebStorage manager shared instance. + static AndroidWebStorageManager instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidWebStorageManager _init() { + _instance = AndroidWebStorageManager(AndroidWebStorageManagerCreationParams( + const PlatformWebStorageManagerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future> getOrigins() async { + List originsList = []; + + Map args = {}; + List> origins = + (await channel?.invokeMethod('getOrigins', args)) + ?.cast>() ?? + []; + + for (var origin in origins) { + originsList.add(WebStorageOrigin( + origin: origin["origin"], + quota: origin["quota"], + usage: origin["usage"])); + } + + return originsList; + } + + @override + Future deleteAllData() async { + Map args = {}; + await channel?.invokeMethod('deleteAllData', args); + } + + @override + Future deleteOrigin({required String origin}) async { + Map args = {}; + args.putIfAbsent("origin", () => origin); + await channel?.invokeMethod('deleteOrigin', args); + } + + @override + Future getQuotaForOrigin({required String origin}) async { + Map args = {}; + args.putIfAbsent("origin", () => origin); + return await channel?.invokeMethod('getQuotaForOrigin', args) ?? 0; + } + + @override + Future getUsageForOrigin({required String origin}) async { + Map args = {}; + args.putIfAbsent("origin", () => origin); + return await channel?.invokeMethod('getUsageForOrigin', args) ?? 0; + } + + @override + void dispose() { + // empty + } +} + +extension InternalWebStorageManager on AndroidWebStorageManager { + get handleMethod => _handleMethod; +} diff --git a/packages/flutter_inappwebview_android/lib/src/webview_asset_loader.dart b/packages/flutter_inappwebview_android/lib/src/webview_asset_loader.dart new file mode 100644 index 0000000..64ca3ec --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/webview_asset_loader.dart @@ -0,0 +1,237 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidPathHandler]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformPathHandlerCreationParams] for +/// more information. +@immutable +class AndroidPathHandlerCreationParams + extends PlatformPathHandlerCreationParams { + /// Creates a new [AndroidPathHandlerCreationParams] instance. + AndroidPathHandlerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformPathHandlerCreationParams params, + ) : super(path: params.path); + + /// Creates a [AndroidPathHandlerCreationParams] instance based on [PlatformPathHandlerCreationParams]. + factory AndroidPathHandlerCreationParams.fromPlatformPathHandlerCreationParams( + PlatformPathHandlerCreationParams params) { + return AndroidPathHandlerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformPathHandler} +abstract mixin class AndroidPathHandler + implements ChannelController, PlatformPathHandler { + final String _id = IdGenerator.generate(); + + @override + late final PlatformPathHandlerEvents? eventHandler; + + @override + late final String path; + + void _init(PlatformPathHandlerCreationParams params) { + this.path = params.path; + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_custompathhandler_${_id}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "handle": + String path = call.arguments["path"]; + return (await eventHandler?.handle(path))?.toMap(); + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Map toMap() { + return {"path": path, "type": type, "id": _id}; + } + + @override + Map toJson() { + return toMap(); + } + + @override + String toString() { + return 'AndroidPathHandler{path: $path, type: $type}'; + } + + @override + void dispose() { + disposeChannel(); + eventHandler = null; + } +} + +/// Object specifying creation parameters for creating a [AndroidAssetsPathHandler]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformAssetsPathHandlerCreationParams] for +/// more information. +@immutable +class AndroidAssetsPathHandlerCreationParams + extends PlatformAssetsPathHandlerCreationParams { + /// Creates a new [AndroidAssetsPathHandlerCreationParams] instance. + AndroidAssetsPathHandlerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformAssetsPathHandlerCreationParams params, + ) : super(params); + + /// Creates a [AndroidAssetsPathHandlerCreationParams] instance based on [PlatformAssetsPathHandlerCreationParams]. + factory AndroidAssetsPathHandlerCreationParams.fromPlatformAssetsPathHandlerCreationParams( + PlatformAssetsPathHandlerCreationParams params) { + return AndroidAssetsPathHandlerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformAssetsPathHandler} +class AndroidAssetsPathHandler extends PlatformAssetsPathHandler + with AndroidPathHandler, ChannelController { + /// Constructs a [AndroidAssetsPathHandler]. + AndroidAssetsPathHandler(PlatformAssetsPathHandlerCreationParams params) + : super.implementation( + params is AndroidAssetsPathHandlerCreationParams + ? params + : AndroidAssetsPathHandlerCreationParams + .fromPlatformAssetsPathHandlerCreationParams(params), + ) { + _init(params); + } +} + +/// Object specifying creation parameters for creating a [AndroidResourcesPathHandler]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformResourcesPathHandlerCreationParams] for +/// more information. +@immutable +class AndroidResourcesPathHandlerCreationParams + extends PlatformResourcesPathHandlerCreationParams { + /// Creates a new [AndroidResourcesPathHandlerCreationParams] instance. + AndroidResourcesPathHandlerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformResourcesPathHandlerCreationParams params, + ) : super(params); + + /// Creates a [AndroidResourcesPathHandlerCreationParams] instance based on [PlatformResourcesPathHandlerCreationParams]. + factory AndroidResourcesPathHandlerCreationParams.fromPlatformResourcesPathHandlerCreationParams( + PlatformResourcesPathHandlerCreationParams params) { + return AndroidResourcesPathHandlerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformResourcesPathHandler} +class AndroidResourcesPathHandler extends PlatformResourcesPathHandler + with AndroidPathHandler, ChannelController { + /// Constructs a [AndroidResourcesPathHandler]. + AndroidResourcesPathHandler(PlatformResourcesPathHandlerCreationParams params) + : super.implementation( + params is AndroidResourcesPathHandlerCreationParams + ? params + : AndroidResourcesPathHandlerCreationParams + .fromPlatformResourcesPathHandlerCreationParams(params), + ) { + _init(params); + } +} + +/// Object specifying creation parameters for creating a [AndroidInternalStoragePathHandler]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformInternalStoragePathHandlerCreationParams] for +/// more information. +@immutable +class AndroidInternalStoragePathHandlerCreationParams + extends PlatformInternalStoragePathHandlerCreationParams { + /// Creates a new [AndroidInternalStoragePathHandlerCreationParams] instance. + AndroidInternalStoragePathHandlerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformInternalStoragePathHandlerCreationParams params, + ) : super(params, directory: params.directory); + + /// Creates a [AndroidInternalStoragePathHandlerCreationParams] instance based on [PlatformInternalStoragePathHandlerCreationParams]. + factory AndroidInternalStoragePathHandlerCreationParams.fromPlatformInternalStoragePathHandlerCreationParams( + PlatformInternalStoragePathHandlerCreationParams params) { + return AndroidInternalStoragePathHandlerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformInternalStoragePathHandler} +class AndroidInternalStoragePathHandler + extends PlatformInternalStoragePathHandler + with AndroidPathHandler, ChannelController { + /// Constructs a [AndroidInternalStoragePathHandler]. + AndroidInternalStoragePathHandler( + PlatformInternalStoragePathHandlerCreationParams params) + : super.implementation( + params is AndroidInternalStoragePathHandlerCreationParams + ? params + : AndroidInternalStoragePathHandlerCreationParams + .fromPlatformInternalStoragePathHandlerCreationParams(params), + ) { + _init(params); + } + + AndroidInternalStoragePathHandlerCreationParams get _internalParams => + params as AndroidInternalStoragePathHandlerCreationParams; + + @override + String get directory => _internalParams.directory; + + @override + Map toMap() { + return {...toMap(), 'directory': directory}; + } +} + +/// Object specifying creation parameters for creating a [AndroidCustomPathHandler]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformCustomPathHandlerCreationParams] for +/// more information. +@immutable +class AndroidCustomPathHandlerCreationParams + extends PlatformCustomPathHandlerCreationParams { + /// Creates a new [AndroidCustomPathHandlerCreationParams] instance. + AndroidCustomPathHandlerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformCustomPathHandlerCreationParams params, + ) : super(params); + + /// Creates a [AndroidCustomPathHandlerCreationParams] instance based on [PlatformCustomPathHandlerCreationParams]. + factory AndroidCustomPathHandlerCreationParams.fromPlatformCustomPathHandlerCreationParams( + PlatformCustomPathHandlerCreationParams params) { + return AndroidCustomPathHandlerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformCustomPathHandler} +class AndroidCustomPathHandler extends PlatformCustomPathHandler + with AndroidPathHandler, ChannelController { + /// Constructs a [AndroidCustomPathHandler]. + AndroidCustomPathHandler(PlatformCustomPathHandlerCreationParams params) + : super.implementation( + params is AndroidCustomPathHandlerCreationParams + ? params + : AndroidCustomPathHandlerCreationParams + .fromPlatformCustomPathHandlerCreationParams(params), + ) { + _init(params); + } +} diff --git a/packages/flutter_inappwebview_android/lib/src/webview_feature.dart b/packages/flutter_inappwebview_android/lib/src/webview_feature.dart new file mode 100644 index 0000000..9b14951 --- /dev/null +++ b/packages/flutter_inappwebview_android/lib/src/webview_feature.dart @@ -0,0 +1,89 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebViewFeature]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebViewFeatureCreationParams] for +/// more information. +@immutable +class AndroidWebViewFeatureCreationParams + extends PlatformWebViewFeatureCreationParams { + /// Creates a new [AndroidWebViewFeatureCreationParams] instance. + const AndroidWebViewFeatureCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformWebViewFeatureCreationParams params, + ) : super(); + + /// Creates a [AndroidWebViewFeatureCreationParams] instance based on [PlatformWebViewFeatureCreationParams]. + factory AndroidWebViewFeatureCreationParams.fromPlatformWebViewFeatureCreationParams( + PlatformWebViewFeatureCreationParams params) { + return AndroidWebViewFeatureCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebViewFeature} +class AndroidWebViewFeature extends PlatformWebViewFeature + with ChannelController { + /// Creates a new [AndroidWebViewFeature]. + AndroidWebViewFeature(PlatformWebViewFeatureCreationParams params) + : super.implementation( + params is AndroidWebViewFeatureCreationParams + ? params + : AndroidWebViewFeatureCreationParams + .fromPlatformWebViewFeatureCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_webviewfeature'); + handler = handleMethod; + initMethodCallHandler(); + } + + factory AndroidWebViewFeature.static() { + return instance(); + } + + static AndroidWebViewFeature? _instance; + + ///Gets the [AndroidWebViewFeature] shared instance. + static AndroidWebViewFeature instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidWebViewFeature _init() { + _instance = AndroidWebViewFeature(AndroidWebViewFeatureCreationParams( + const PlatformWebViewFeatureCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future isFeatureSupported(WebViewFeature feature) async { + Map args = {}; + args.putIfAbsent("feature", () => feature.toNativeValue()); + return await channel?.invokeMethod('isFeatureSupported', args) ?? + false; + } + + @override + Future isStartupFeatureSupported(WebViewFeature startupFeature) async { + Map args = {}; + args.putIfAbsent("startupFeature", () => startupFeature.toNativeValue()); + return await channel?.invokeMethod( + 'isStartupFeatureSupported', args) ?? + false; + } + + @override + void dispose() { + // empty + } +} + +extension InternalWebViewFeature on AndroidWebViewFeature { + get handleMethod => _handleMethod; +} diff --git a/packages/flutter_inappwebview_android/pubspec.yaml b/packages/flutter_inappwebview_android/pubspec.yaml new file mode 100644 index 0000000..912f92e --- /dev/null +++ b/packages/flutter_inappwebview_android/pubspec.yaml @@ -0,0 +1,83 @@ +name: flutter_inappwebview_android +description: Android implementation of the flutter_inappwebview plugin. +version: 1.1.3 +homepage: https://inappwebview.dev/ +repository: https://github.com/pichillilorenzo/flutter_inappwebview/tree/master/flutter_inappwebview_android +issue_tracker: https://github.com/pichillilorenzo/flutter_inappwebview/issues +funding: + - https://inappwebview.dev/donate/ +topics: + - html + - webview + - webview-flutter + - inappwebview + - browser + +environment: + sdk: ^3.5.0 + flutter: ">=3.24.0" + +dependencies: + flutter: + sdk: flutter + flutter_inappwebview_platform_interface: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + plugin_platform_interface: ^2.1.8 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + implements: flutter_inappwebview + platforms: + android: + package: com.pichillilorenzo.flutter_inappwebview_android + pluginClass: InAppWebViewFlutterPlugin + dartPluginClass: AndroidInAppWebViewPlatform + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/flutter_inappwebview_android/test/flutter_inappwebview_android_test.dart b/packages/flutter_inappwebview_android/test/flutter_inappwebview_android_test.dart new file mode 100644 index 0000000..e69de29 diff --git a/packages/whisper_ggml/JJ_PATCH.md b/packages/whisper_ggml/JJ_PATCH.md index cc11ce0..0dfa68c 100644 --- a/packages/whisper_ggml/JJ_PATCH.md +++ b/packages/whisper_ggml/JJ_PATCH.md @@ -14,3 +14,8 @@ Windows 에서 음성인식이 끝나는 순간 앱이 종료됨 (예외 코드 - example 폴더 제거 원본 패키지에서 고쳐지면 pubspec.yaml 을 다시 `whisper_ggml: ^버전` 으로 바꾸면 된다. + +## Android: FFmpeg 하나만 넣기 +- pubspec.yaml · lib/src/whisper_audio_convert.dart: `ffmpeg_kit_flutter_new_min` → `ffmpeg_kit_flutter_new_min_gpl` + (앱이 MKV 만들기에 쓰는 것과 같은 것. 두 가지가 함께 들어가면 같은 이름의 Java 클래스 · 네이티브 라이브러리가 겹쳐 빌드 · 실행이 안 됨) +- android/build.gradle: compileSdk 34 → 36 (ffmpeg-kit min-gpl 이 35 이상을 요구) diff --git a/packages/whisper_ggml/android/build.gradle b/packages/whisper_ggml/android/build.gradle index b807a42..e456883 100644 --- a/packages/whisper_ggml/android/build.gradle +++ b/packages/whisper_ggml/android/build.gradle @@ -26,7 +26,7 @@ android { namespace "com.devac.whisper_ggml" ndkVersion "29.0.13113456" - compileSdk 34 + compileSdk 36 // Invoke the shared CMake build with the Android Gradle Plugin. externalNativeBuild { diff --git a/packages/whisper_ggml/lib/src/whisper_audio_convert.dart b/packages/whisper_ggml/lib/src/whisper_audio_convert.dart index 5b2c3e9..2225131 100644 --- a/packages/whisper_ggml/lib/src/whisper_audio_convert.dart +++ b/packages/whisper_ggml/lib/src/whisper_audio_convert.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'package:ffmpeg_kit_flutter_new_min/ffmpeg_kit.dart'; -import 'package:ffmpeg_kit_flutter_new_min/ffmpeg_session.dart'; -import 'package:ffmpeg_kit_flutter_new_min/return_code.dart'; +import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit.dart'; +import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_session.dart'; +import 'package:ffmpeg_kit_flutter_new_min_gpl/return_code.dart'; import 'package:flutter/foundation.dart'; import 'package:universal_io/io.dart'; diff --git a/packages/whisper_ggml/pubspec.yaml b/packages/whisper_ggml/pubspec.yaml index 8275ea1..b772c62 100644 --- a/packages/whisper_ggml/pubspec.yaml +++ b/packages/whisper_ggml/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: sdk: flutter ffi: ^2.1.4 - ffmpeg_kit_flutter_new_min: ^2.1.0 + ffmpeg_kit_flutter_new_min_gpl: ^2.6.2 freezed_annotation: ^3.0.0 json_annotation: ^4.9.0 plugin_platform_interface: ^2.1.8 diff --git a/pubspec.lock b/pubspec.lock index 6840a8b..4cb939d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -161,14 +161,13 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.2" - ffmpeg_kit_flutter_new_min: - dependency: transitive + ffmpeg_kit_flutter_new_min_gpl: + dependency: "direct main" description: - name: ffmpeg_kit_flutter_new_min - sha256: "1fce79b73ebbcf34e1063ed84cacc4920adfbb8631877e585302e68157b0ace3" - url: "https://pub.dev" - source: hosted - version: "2.1.0" + path: "packages/ffmpeg_kit_flutter_new_min_gpl" + relative: true + source: path + version: "2.6.2" ffmpeg_kit_flutter_platform_interface: dependency: transitive description: @@ -252,12 +251,11 @@ packages: source: hosted version: "6.1.5" flutter_inappwebview_android: - dependency: transitive + dependency: "direct overridden" description: - name: flutter_inappwebview_android - sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" - url: "https://pub.dev" - source: hosted + path: "packages/flutter_inappwebview_android" + relative: true + source: path version: "1.1.3" flutter_inappwebview_internal_annotations: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 8f17cb6..d97ac67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,15 @@ dependencies: package_info_plus: ^10.2.1 crypto: ^3.0.7 flutter_inappwebview: ^6.1.5 + ffmpeg_kit_flutter_new_min_gpl: 2.6.2 +# Android 빌드 도구 (AGP 9) 에 맞게 고친 사본 (packages/flutter_inappwebview_android/JJ_PATCH.md) +dependency_overrides: + flutter_inappwebview_android: + path: packages/flutter_inappwebview_android + # Android 만 (Windows 에 FFmpeg DLL 을 넣지 않도록, packages/ffmpeg_kit_flutter_new_min_gpl/JJ_PATCH.md) + ffmpeg_kit_flutter_new_min_gpl: + path: packages/ffmpeg_kit_flutter_new_min_gpl + dev_dependencies: flutter_test: sdk: flutter