diff --git a/.analysis_options b/.analysis_options deleted file mode 100644 index 518eb901..00000000 --- a/.analysis_options +++ /dev/null @@ -1,2 +0,0 @@ -analyzer: - strong-mode: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/BUG.md b/.github/ISSUE_TEMPLATE/BUG.md new file mode 100644 index 00000000..fe34c4b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG.md @@ -0,0 +1,73 @@ +--- +name: I have found out bug in plugin. +about: You are writing an Flutter application with this webview plugin but the application is crashing + or throws an exception, a plugin is buggy, or something looks wrong. +title: '' +labels: '' +assignees: '' + +--- + + + +## System info + +Issue occurs on: iOS / Android / both +Plugin version: xxx +Flutter doctor output: + +``` +paste it here... +``` + +## Steps to Reproduce + + + +1. ... +2. ... +3. ... + +## Logs + + + +``` +``` + + + +``` +``` + + + +``` +``` \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/SUPPORT.md b/.github/ISSUE_TEMPLATE/SUPPORT.md new file mode 100644 index 00000000..80b8ac10 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/SUPPORT.md @@ -0,0 +1,25 @@ +--- +name: I want help writing my application +about: You have a question for how to achieve a particular effect, or you need help + with using a particular API. +title: '' +labels: '' +assignees: '' + +--- + + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..ff69d16c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,46 @@ +--- +name: Feature request +about: Suggest a new idea for Flutter webview plugin. +title: '' +labels: '' +assignees: '' + +--- + + + +## Use case + + + +## Proposal + + \ No newline at end of file diff --git a/.github/workflows/android_ut.yml b/.github/workflows/android_ut.yml new file mode 100644 index 00000000..393daccb --- /dev/null +++ b/.github/workflows/android_ut.yml @@ -0,0 +1,20 @@ +name: Android Unit Tests + +on: [push, pull_request] + +jobs: + test: + name: Linux Android Unit Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-java@v1 + with: + java-version: '12.x' + - uses: subosito/flutter-action@v1 + with: + flutter-version: '1.7.8+hotfix.4' + - run: flutter doctor + - run: flutter pub get + - run: sh android_test.sh diff --git a/.github/workflows/flutter_ut.yml b/.github/workflows/flutter_ut.yml new file mode 100644 index 00000000..42d66d46 --- /dev/null +++ b/.github/workflows/flutter_ut.yml @@ -0,0 +1,20 @@ +name: Flutter Unit Tests + +on: [push, pull_request] + +jobs: + test: + name: Linux Flutter Unit Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-java@v1 + with: + java-version: '12.x' + - uses: subosito/flutter-action@v1 + with: + flutter-version: '1.7.8+hotfix.4' + - run: flutter doctor + - run: flutter pub get + - run: flutter test diff --git a/.gitignore b/.gitignore index 731501dd..9f1cce46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,20 @@ .DS_Store .atom/ .idea +.vscode .packages .pub/ +.gradle/ build/ ios/.generated/ packages pubspec.lock example/ios/Podfile.lock +**/Flutter/App.framework/ +**/Flutter/Flutter.framework/ +**/Flutter/Generated.xcconfig/ +**/Flutter/flutter_assets/ +example/ios/Flutter/flutter_export_environment.sh +android/.project +android/.settings/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f348c4..7aaa09ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,142 @@ +# 0.3.9+1 + +- Fixed error methods on iOS + +# 0.3.9 + +- Fixed error methods on iOS +- fixed build +- fixed ios clean cookies +- 4 Make plugin work in headless mode when extending FlutterApplication +- added canGoBack and canGoForward methods + +# 0.3.8 + +- Fix iOS local URL support (fixes #114) +- bugfix: Added google() repository to allprojects to satisfy androidx build rules +- fixed min sdk for android + +# 0.3.7 + +- Added reloading url with headers +- Added support for reloading url with headers + +# 0.3.6 + +- Allow web contents debugging in Chrome +- Android: allow geolocation and file chooser simultaneously +- Add min sdk requirement and descriptions +- fix bug android webview httperror exception +- Exposes displayZoomControls, withOverviewMode and useWideViewPort settings for Android WebView + +# 0.3.5 + +- Ability to choose from camera or gallery when using +- Support for webview’s estimated loading progress #255 +- Fix back button handler to be compatible with the WillPopScope widget + +# 0.3.4 + +- WebView always hidden on iOS + +# 0.3.3 + +- BREAKING CHANGE - AndroidX support + +# 0.3.2 + +- enable Javascript in iOS, support abort loading specific URLs +- add resizeToAvoidBottomInset to WebviewScaffold; #301 + +# 0.3.1 + +- Add support for geolocation Android +- fix No269: Can't load target="_blank" links on iOS +- fix: reloadUrl will not return Future +- Fix height of keyboard +- Fix Hide/Show WebView +- hotfix widget back to initialChild after webview is tapped on Android + +# 0.3.0 + +- Fixes rect capture issue. Ensures WebView remains in the correct place on screen even when keyboard appears. +- Fixed iOS crash issue with Flutter `>= 0.10.2`. +- Added new `clearCookies` feature. +- Added support for `hidden` and `initialChild` feature to show page loading view. +- Added supportMultipleWindows: enables Multiple Window Support on Android. +- Added appCacheEnabled: enables Application Caches API on Android. +- Added allowFileURLs: allows `file://` local file URLs. +- iOS Now supports: `reload`, `goBack`, and `goForward`. +- iOS Bug fix `didFailNavigation` #77 +- Updated Android `compileSdkVersion` to `27` matching offical Flutter plugins. +- Fixed Android `reloadUrl` so settings are not cleared. +- Enabled compatible `Mixed Content Mode` on Android. + +# 0.2.1 + +- Added webview scrolling listener +- Added stopLoading() method + +# 0.2.0 + +- update sdk +- prevent negative webview height in scaffold +- handle type error in getCookies +- Support file upload via WebView on Android +- fix WebviewScaffold crash on iOS +- Scrollbar functionality to Web view +- Add support of HTTP errors +- Add headers when loading url + +# 0.1.6 + +- fix onStateChanged +- Taking safe areas into account for bottom bars +- iOS + + withLocalUrl option for iOS > 9.0 +- Android + + add reload, goBack and foForward function + +# 0.1.5 + +- iOS use WKWebView instead of UIWebView + +# 0.1.4 + +- support localstorage for ANDROID + +# 0.1.3 + +- support zoom in webview + +# 0.1.2 + +- support bottomNavigationBar and persistentFooterButtons on webview scaffold + +# 0.1.1 +- support back button navigation for Android + + if cannot go back, it will trigger onDestroy +- support preview dart2 + +# 0.1.0+1 + +- fix Android close webview + +# 0.1.0 + +- iOS && Android: + - get cookies + - eval javascript + - user agent setting + - state change event + - embed in rectangle or fullscreen if null + - hidden webview + +- Android + - adding Activity in manifest is not needed anymore + +- Add `WebviewScaffold` + # 0.0.9 - Android: remove the need to use FlutterActivity as base activity diff --git a/LICENSE b/LICENSE index 86928f65..2fe58f3d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -// Copyright 2017 Your Company. All rights reserved. +// Copyright 2017 Hadrien Lejard. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are @@ -10,7 +10,7 @@ // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. -// * Neither the name of Your Company nor the names of its +// * Neither the name of Hadrien Lejard nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // diff --git a/README.md b/README.md index bccca4fc..7bb5abeb 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,261 @@ -# flutter_webview_plugin +[![Flutter Community: flutter_webview_plugin](https://fluttercommunity.dev/_github/header/flutter_webview_plugin)](https://github.com/fluttercommunity/community) -Plugin that allow Flutter to communicate with a native WebView. +# Flutter WebView Plugin -***For Android, it will launch a new Activity inside the App with the Webview inside. Does not allow to integrate a Webview inside a Flutter Widget*** +[![pub package](https://img.shields.io/pub/v/flutter_webview_plugin.svg)](https://pub.dartlang.org/packages/flutter_webview_plugin) -***For IOS, it will launch a new UIViewController inside the App with the UIWebView inside. Does not allow to integrate a Webview inside a Flutter Widget*** +Plugin that allows Flutter to communicate with a native WebView. - - [x] Android - - [x] IOS +**_Warning:_** +The webview is not integrated in the widget tree, it is a native view on top of the flutter view. +You won't be able see snackbars, dialogs, or other flutter widgets that would overlap with the region of the screen taken up by the webview. + +The getSafeAcceptedType() function is available only for minimum SDK of 21. +eval() function only supports SDK of 19 or greater for evaluating Javascript. ## Getting Started For help getting started with Flutter, view our online [documentation](http://flutter.io/). +#### iOS + +In order for plugin to work correctly, you need to add new key to `ios/Runner/Info.plist` + +```xml +NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsArbitraryLoadsInWebContent + + +``` + +`NSAllowsArbitraryLoadsInWebContent` is for iOS 10+ and `NSAllowsArbitraryLoads` for iOS 9. + + ### How it works -#### Launch WebView with variable url +#### Launch WebView Fullscreen with Flutter navigation ```dart -void launchWebView(String url) sync { - var flutterWebviewPlugin = new FlutterWebviewPlugin(); - - flutterWebviewPlugin.launch(url); - - // Wait in this async function until destroy of WebView. - await flutterWebviewPlugin.onDestroy.first; -} +new MaterialApp( + routes: { + "/": (_) => new WebviewScaffold( + url: "https://www.google.com", + appBar: new AppBar( + title: new Text("Widget webview"), + ), + ), + }, + ); +``` + +Optional parameters `hidden` and `initialChild` are available so that you can show something else while waiting for the page to load. +If you set `hidden` to true it will show a default CircularProgressIndicator. If you additionally specify a Widget for initialChild +you can have it display whatever you like till page-load. + +e.g. The following will show a read screen with the text 'waiting.....'. +```dart +return new MaterialApp( + title: 'Flutter WebView Demo', + theme: new ThemeData( + primarySwatch: Colors.blue, + ), + routes: { + '/': (_) => const MyHomePage(title: 'Flutter WebView Demo'), + '/widget': (_) => new WebviewScaffold( + url: selectedUrl, + appBar: new AppBar( + title: const Text('Widget webview'), + ), + withZoom: true, + withLocalStorage: true, + hidden: true, + initialChild: Container( + color: Colors.redAccent, + child: const Center( + child: Text('Waiting.....'), + ), + ), + ), + }, +); +``` + +`FlutterWebviewPlugin` provide a singleton instance linked to one unique webview, +so you can take control of the webview from anywhere in the app + +listen for events + +```dart +final flutterWebviewPlugin = new FlutterWebviewPlugin(); + +flutterWebviewPlugin.onUrlChanged.listen((String url) { + +}); +``` + +#### Listen for scroll event in webview + +```dart +final flutterWebviewPlugin = new FlutterWebviewPlugin(); +flutterWebviewPlugin.onScrollYChanged.listen((double offsetY) { // latest offset value in vertical scroll + // compare vertical scroll changes here with old value +}); + +flutterWebviewPlugin.onScrollXChanged.listen((double offsetX) { // latest offset value in horizontal scroll + // compare horizontal scroll changes here with old value +}); + +```` + +Note: Do note there is a slight difference is scroll distance between ios and android. Android scroll value difference tends to be larger than ios devices. + + +#### Hidden WebView + +```dart +final flutterWebviewPlugin = new FlutterWebviewPlugin(); + +flutterWebviewPlugin.launch(url, hidden: true); +``` + +#### Close launched WebView + +```dart +flutterWebviewPlugin.close(); ``` -### Close launched WebView +#### Webview inside custom Rectangle ```dart -void launchWebViewAndCloseAfterWhile(String url) { - var flutterWebviewPlugin = new FlutterWebviewPlugin(); - - flutterWebviewPlugin.launch(url); - - // After 10 seconds. - new Timer(const Duration(seconds: 10), () { - // Close WebView. - // This will also emit the onDestroy event. - flutterWebviewPlugin.close(); +final flutterWebviewPlugin = new FlutterWebviewPlugin(); + +flutterWebviewPlugin.launch(url, + fullScreen: false, + rect: new Rect.fromLTWH( + 0.0, + 0.0, + MediaQuery.of(context).size.width, + 300.0, + ), +); +``` + +#### Injecting custom code into the webview +Use `flutterWebviewPlugin.evalJavaScript(String code)`. This function must be run after the page has finished loading (i.e. listen to `onStateChanged` for events where state is `finishLoad`). + +If you have a large amount of JavaScript to embed, use an asset file. Add the asset file to `pubspec.yaml`, then call the function like: + +```dart +Future loadJS(String name) async { + var givenJS = rootBundle.loadString('assets/$name.js'); + return givenJS.then((String js) { + flutterWebViewPlugin.onStateChanged.listen((viewState) async { + if (viewState.type == WebViewState.finishLoad) { + flutterWebViewPlugin.evalJavascript(js); + } + }); }); } ``` -### Android +### Accessing local files in the file system +Set the `withLocalUrl` option to true in the launch function or in the Webview scaffold to enable support for local URLs. -Add the Activity to you `AndroidManifest.xml`: +Note that, on iOS, the `localUrlScope` option also needs to be set to a path to a directory. All files inside this folder (or subfolder) will be allowed access. If ommited, only the local file being opened will have access allowed, resulting in no subresources being loaded. This option is ignored on Android. -```xml - +### Webview Events + +- `Stream` onDestroy +- `Stream` onUrlChanged +- `Stream` onStateChanged +- `Stream` onScrollXChanged +- `Stream` onScrollYChanged +- `Stream` onError + +**_Don't forget to dispose webview_** +`flutterWebviewPlugin.dispose()` + +### Webview Functions + +```dart +Future launch(String url, { + Map headers: null, + bool withJavascript: true, + bool clearCache: false, + bool clearCookies: false, + bool hidden: false, + bool enableAppScheme: true, + Rect rect: null, + String userAgent: null, + bool withZoom: false, + bool withLocalStorage: true, + bool withLocalUrl: true, + String localUrlScope: null, + bool scrollBar: true, + bool supportMultipleWindows: false, + bool appCacheEnabled: false, + bool allowFileURLs: false, + bool displayZoomControls: false, + bool useWideViewPort: false, + bool withOverviewMode: false, +}); +``` + +```dart +Future evalJavascript(String code); +``` + +```dart +Future> getCookies(); +``` + +```dart +Future cleanCookies(); +``` + +```dart +Future resize(Rect rect); +``` + +```dart +Future show(); +``` + +```dart +Future hide(); ``` -### iOS +```dart +Future reloadUrl(String url); +``` + +```dart +Future close(); +``` + +```dart +Future reload(); +``` + +```dart +Future goBack(); +``` -No extra configuration is needed. +```dart +Future goForward(); +``` + +```dart +Future stopLoading(); +``` + +```dart +Future canGoBack(); +``` + +```dart +Future canGoForward(); +``` diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..18e07b8f --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,121 @@ +analyzer: + +linter: + rules: + # these rules are documented on and in the same order as + # the Dart Lint rules page to make maintenance easier + # https://github.com/dart-lang/linter/blob/master/example/all.yaml + - always_declare_return_types + - always_put_control_body_on_new_line + # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 + - always_require_non_null_named_parameters + # - always_specify_types + - annotate_overrides + # - avoid_annotating_with_dynamic # conflicts with always_specify_types + - avoid_as + # - avoid_bool_literals_in_conditional_expressions # not yet tested + # - avoid_catches_without_on_clauses # we do this commonly + # - avoid_catching_errors # we do this commonly + - avoid_classes_with_only_static_members + # - avoid_double_and_int_checks # only useful when targeting JS runtime + - avoid_empty_else + - avoid_field_initializers_in_const_classes + # - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + # - avoid_js_rounded_ints # only useful when targeting JS runtime + - avoid_null_checks_in_equality_operators + # - avoid_positional_boolean_parameters # not yet tested + # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + # - avoid_returning_null # we do this commonly + # - avoid_returning_this # https://github.com/dart-lang/linter/issues/842 + # - avoid_setters_without_getters # not yet tested + # - avoid_single_cascade_in_expression_statements # not yet tested + - avoid_slow_async_io + # - avoid_types_as_parameter_names # https://github.com/dart-lang/linter/pull/954/files + # - avoid_types_on_closure_parameters # conflicts with always_specify_types + # - avoid_unused_constructor_parameters # https://github.com/dart-lang/linter/pull/847 + - await_only_futures + - camel_case_types + - cancel_subscriptions + # - cascade_invocations # not yet tested + # - close_sinks # https://github.com/flutter/flutter/issues/5789 + # - comment_references # blocked on https://github.com/dart-lang/dartdoc/issues/1153 + # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 + - control_flow_in_finally + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - hash_and_equals + - implementation_imports + # - invariant_booleans # https://github.com/flutter/flutter/issues/5790 + - iterable_contains_unrelated_type + # - join_return_with_assignment # not yet tested + - library_names + - library_prefixes + - list_remove_unrelated_type + # - literal_only_boolean_expressions # https://github.com/flutter/flutter/issues/5791 + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + # - omit_local_variable_types # opposite of always_specify_types + # - one_member_abstracts # too many false positives + # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + # - parameter_assignments # we do this commonly + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_bool_in_asserts + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + # - prefer_constructors_over_static_methods # not yet tested + - prefer_contains + - prefer_equal_for_default_values + # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods + - prefer_final_fields + - prefer_final_locals + # - prefer_foreach + # - prefer_function_declarations_over_variables # not yet tested + - prefer_initializing_formals + # - prefer_interpolation_to_compose_strings # not yet tested + # - prefer_iterable_whereType # https://github.com/dart-lang/sdk/issues/32463 + - prefer_is_empty + - prefer_is_not_empty + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - sort_constructors_first + - sort_unnamed_constructors_first + - super_goes_last + - test_types_in_equals + - throw_in_finally + # - type_annotate_public_apis # subset of always_specify_types + - type_init_formals + # - unawaited_futures # https://github.com/flutter/flutter/issues/5793 + - unnecessary_brace_in_string_interps + - unnecessary_getters_setters + # - unnecessary_lambdas # https://github.com/dart-lang/linter/issues/498 + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + # - unnecessary_statements # not yet tested + - unnecessary_this + - unrelated_type_equality_checks + - use_rethrow_when_possible + # - use_setters_to_change_properties # not yet tested + # - use_string_buffers # https://github.com/dart-lang/linter/pull/664 + # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review + - valid_regexps + # - void_checks # not yet tested \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 5c4ef828..00000000 --- a/android/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures - -/gradle -/gradlew -/gradlew.bat diff --git a/android/android.iml b/android/android.iml new file mode 100644 index 00000000..0fe7ea9f --- /dev/null +++ b/android/android.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index b308a6cd..ec4bab9e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,32 +1,71 @@ -group 'com.yourcompany.flutter_webview_plugin' +group 'com.flutter_webview_plugin' version '1.0-SNAPSHOT' +def ANDROIDX_WARNING = "flutterPluginsAndroidXWarning"; +gradle.buildFinished { buildResult -> + if (buildResult.failure && !rootProject.ext.has(ANDROIDX_WARNING)) { + println ' *********************************************************' + println 'WARNING: This version of flutter_webview_plugin will break your Android build if it or its dependencies aren\'t compatible with AndroidX.' + println ' See https://goo.gl/CP92wY for more information on the problem and how to fix it.' + println ' This warning prints for all Android build failures. The real root cause of the error may be unrelated.' + println ' *********************************************************' + rootProject.ext.set(ANDROIDX_WARNING, true); + } +} + buildscript { repositories { + google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:3.3.2' } } allprojects { repositories { jcenter() + google() } } apply plugin: 'com.android.library' android { - compileSdkVersion 25 - buildToolsVersion '25.0.0' + compileSdkVersion 28 defaultConfig { - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + // NOTE(jeffmikels): When targetSdkVersion or minSdkVersion is not set or < 4, gradle adds + // additional scary permissions such as WRITE_EXTERNAL_STORAGE and READ_PHONE_STATE. + minSdkVersion 16 } lintOptions { disable 'InvalidPackage' } + + testOptions { + unitTests { + includeAndroidResources = true + } + } +} +dependencies { + implementation 'androidx.appcompat:appcompat:1.0.2' + + testImplementation 'junit:junit:4.12' + testImplementation 'androidx.test:core:1.2.0' + +// When running unit tests for project, gradle needs to have flutter.jar in path +// since there's no FLUTTER_HOME variable, we need to pass flutterPath from console with command: +// ./gradlew test -DflutterPath=/Users/rafal.wachol/Utils/flutter +// +// while develop you can set path to this jar explicitly so IDE won't complain + if(System.getProperty('flutterPath')) { + testImplementation files(System.getProperty('flutterPath') + '/bin/cache/artifacts/engine/android-x64/flutter.jar') + } + + testImplementation 'org.mockito:mockito-inline:2.28.2' } diff --git a/android/flutter_webview_plugin.iml b/android/flutter_webview_plugin.iml new file mode 100644 index 00000000..81a83b99 --- /dev/null +++ b/android/flutter_webview_plugin.iml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/local.properties b/android/local.properties new file mode 100644 index 00000000..39a22f1c --- /dev/null +++ b/android/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Mon Jun 03 19:01:20 BST 2019 +sdk.dir=C\:\\Users\\Gloria\\AppData\\Local\\Android\\Sdk diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index ec6979f3..20ea4b76 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,7 +1,17 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.flutter_webview_plugin"> - + + + + + diff --git a/android/src/main/java/com/flutter_webview_plugin/BrowserClient.java b/android/src/main/java/com/flutter_webview_plugin/BrowserClient.java new file mode 100644 index 00000000..d3e136e6 --- /dev/null +++ b/android/src/main/java/com/flutter_webview_plugin/BrowserClient.java @@ -0,0 +1,119 @@ +package com.flutter_webview_plugin; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.os.Build; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by lejard_h on 20/12/2017. + */ + +public class BrowserClient extends WebViewClient { + private Pattern invalidUrlPattern = null; + + public BrowserClient() { + this(null); + } + + public BrowserClient(String invalidUrlRegex) { + super(); + if (invalidUrlRegex != null) { + invalidUrlPattern = Pattern.compile(invalidUrlRegex); + } + } + + public void updateInvalidUrlRegex(String invalidUrlRegex) { + if (invalidUrlRegex != null) { + invalidUrlPattern = Pattern.compile(invalidUrlRegex); + } else { + invalidUrlPattern = null; + } + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + Map data = new HashMap<>(); + data.put("url", url); + data.put("type", "startLoad"); + FlutterWebviewPlugin.channel.invokeMethod("onState", data); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + Map data = new HashMap<>(); + data.put("url", url); + + FlutterWebviewPlugin.channel.invokeMethod("onUrlChanged", data); + + data.put("type", "finishLoad"); + FlutterWebviewPlugin.channel.invokeMethod("onState", data); + + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + // returning true causes the current WebView to abort loading the URL, + // while returning false causes the WebView to continue loading the URL as usual. + String url = request.getUrl().toString(); + boolean isInvalid = checkInvalidUrl(url); + Map data = new HashMap<>(); + data.put("url", url); + data.put("type", isInvalid ? "abortLoad" : "shouldStart"); + + FlutterWebviewPlugin.channel.invokeMethod("onState", data); + return isInvalid; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + // returning true causes the current WebView to abort loading the URL, + // while returning false causes the WebView to continue loading the URL as usual. + boolean isInvalid = checkInvalidUrl(url); + Map data = new HashMap<>(); + data.put("url", url); + data.put("type", isInvalid ? "abortLoad" : "shouldStart"); + + FlutterWebviewPlugin.channel.invokeMethod("onState", data); + return isInvalid; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { + super.onReceivedHttpError(view, request, errorResponse); + Map data = new HashMap<>(); + data.put("url", request.getUrl().toString()); + data.put("code", Integer.toString(errorResponse.getStatusCode())); + FlutterWebviewPlugin.channel.invokeMethod("onHttpError", data); + } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + Map data = new HashMap<>(); + data.put("url", failingUrl); + data.put("code", Integer.toString(errorCode)); + FlutterWebviewPlugin.channel.invokeMethod("onHttpError", data); + } + + private boolean checkInvalidUrl(String url) { + if (invalidUrlPattern == null) { + return false; + } else { + Matcher matcher = invalidUrlPattern.matcher(url); + return matcher.lookingAt(); + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java b/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java index 5a19504a..b17c2d6b 100644 --- a/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java +++ b/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java @@ -1,10 +1,21 @@ package com.flutter_webview_plugin; -import android.content.Intent; + import android.app.Activity; import android.content.Context; +import android.content.Intent; +import android.graphics.Point; +import android.view.Display; +import android.webkit.WebStorage; +import android.widget.FrameLayout; +import android.webkit.CookieManager; +import android.webkit.ValueCallback; +import android.os.Build; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; -import io.flutter.app.FlutterActivity; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -13,52 +24,302 @@ /** * FlutterWebviewPlugin */ -public class FlutterWebviewPlugin implements MethodCallHandler { - private Activity activity; - public static MethodChannel channel; - private final int WEBVIEW_ACTIVITY_CODE = 1; - private static final String CHANNEL_NAME = "flutter_webview_plugin"; - - public static void registerWith(PluginRegistry.Registrar registrar) { - channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME); - FlutterWebviewPlugin instance = new FlutterWebviewPlugin((Activity) registrar.activity()); - channel.setMethodCallHandler(instance); - } - - private FlutterWebviewPlugin(Activity activity) { - this.activity = activity; - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - switch (call.method) { - case "launch": - openUrl(call, result); - break; - case "close": - close(call, result); - break; - default: - result.notImplemented(); - break; - } - } - - private void openUrl(MethodCall call, MethodChannel.Result result) { - Intent intent = new Intent(activity, WebviewActivity.class); - - intent.putExtra(WebviewActivity.URL_KEY, (String) call.argument("url")); - intent.putExtra(WebviewActivity.WITH_JAVASCRIPT_KEY, (boolean) call.argument("withJavascript")); - intent.putExtra(WebviewActivity.CLEAR_CACHE_KEY, (boolean) call.argument("clearCache")); - intent.putExtra(WebviewActivity.CLEAR_COOKIES_KEY, (boolean) call.argument("clearCookies")); - - activity.startActivityForResult(intent, WEBVIEW_ACTIVITY_CODE); - - result.success(null); - } - - private void close(MethodCall call, MethodChannel.Result result) { - activity.finishActivity(WEBVIEW_ACTIVITY_CODE); - result.success(null); - } +public class FlutterWebviewPlugin implements MethodCallHandler, PluginRegistry.ActivityResultListener { + private Activity activity; + private WebviewManager webViewManager; + private Context context; + static MethodChannel channel; + private static final String CHANNEL_NAME = "flutter_webview_plugin"; + private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; + + public static void registerWith(PluginRegistry.Registrar registrar) { + if (registrar.activity() != null) { + channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME); + final FlutterWebviewPlugin instance = new FlutterWebviewPlugin(registrar.activity(), registrar.activeContext()); + registrar.addActivityResultListener(instance); + channel.setMethodCallHandler(instance); + } + } + + FlutterWebviewPlugin(Activity activity, Context context) { + this.activity = activity; + this.context = context; + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + switch (call.method) { + case "launch": + openUrl(call, result); + break; + case "close": + close(call, result); + break; + case "eval": + eval(call, result); + break; + case "resize": + resize(call, result); + break; + case "reload": + reload(call, result); + break; + case "back": + back(call, result); + break; + case "forward": + forward(call, result); + break; + case "hide": + hide(call, result); + break; + case "show": + show(call, result); + break; + case "reloadUrl": + reloadUrl(call, result); + break; + case "stopLoading": + stopLoading(call, result); + break; + case "cleanCookies": + cleanCookies(call, result); + break; + case "canGoBack": + canGoBack(result); + break; + case "canGoForward": + canGoForward(result); + break; + case "cleanCache": + cleanCache(result); + break; + default: + result.notImplemented(); + break; + } + } + + private void cleanCache(MethodChannel.Result result) { + webViewManager.cleanCache(); + WebStorage.getInstance().deleteAllData(); + result.success(null); + } + + void openUrl(MethodCall call, MethodChannel.Result result) { + boolean hidden = call.argument("hidden"); + String url = call.argument("url"); + String userAgent = call.argument("userAgent"); + boolean withJavascript = call.argument("withJavascript"); + boolean clearCache = call.argument("clearCache"); + boolean clearCookies = call.argument("clearCookies"); + ArrayList cookies = call.argument("cookies"); + boolean withZoom = call.argument("withZoom"); + boolean displayZoomControls = call.argument("displayZoomControls"); + boolean withLocalStorage = call.argument("withLocalStorage"); + boolean withOverviewMode = call.argument("withOverviewMode"); + boolean supportMultipleWindows = call.argument("supportMultipleWindows"); + boolean appCacheEnabled = call.argument("appCacheEnabled"); + Map headers = call.argument("headers"); + boolean scrollBar = call.argument("scrollBar"); + boolean allowFileURLs = call.argument("allowFileURLs"); + boolean useWideViewPort = call.argument("useWideViewPort"); + String invalidUrlRegex = call.argument("invalidUrlRegex"); + boolean geolocationEnabled = call.argument("geolocationEnabled"); + boolean debuggingEnabled = call.argument("debuggingEnabled"); + + if (webViewManager == null || webViewManager.closed == true) { + Map arguments = (Map) call.arguments; + List channelNames = new ArrayList(); + if (arguments.containsKey(JS_CHANNEL_NAMES_FIELD)) { + channelNames = (List) arguments.get(JS_CHANNEL_NAMES_FIELD); + } + webViewManager = new WebviewManager(activity, context, channelNames); + } + + FrameLayout.LayoutParams params = buildLayoutParams(call); + + activity.addContentView(webViewManager.webView, params); + + webViewManager.openUrl(withJavascript, + clearCache, + hidden, + clearCookies, + cookies, + userAgent, + url, + headers, + withZoom, + displayZoomControls, + withLocalStorage, + withOverviewMode, + scrollBar, + supportMultipleWindows, + appCacheEnabled, + allowFileURLs, + useWideViewPort, + invalidUrlRegex, + geolocationEnabled, + debuggingEnabled + ); + result.success(null); + } + + private FrameLayout.LayoutParams buildLayoutParams(MethodCall call) { + Map rc = call.argument("rect"); + FrameLayout.LayoutParams params; + if (rc != null) { + params = new FrameLayout.LayoutParams( + dp2px(activity, rc.get("width").intValue()), dp2px(activity, rc.get("height").intValue())); + params.setMargins(dp2px(activity, rc.get("left").intValue()), dp2px(activity, rc.get("top").intValue()), + 0, 0); + } else { + Display display = activity.getWindowManager().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + int width = size.x; + int height = size.y; + params = new FrameLayout.LayoutParams(width, height); + } + + return params; + } + + private void stopLoading(MethodCall call, MethodChannel.Result result) { + if (webViewManager != null) { + webViewManager.stopLoading(call, result); + } + result.success(null); + } + + void close(MethodCall call, MethodChannel.Result result) { + if (webViewManager != null) { + webViewManager.close(call, result); + webViewManager = null; + } + } + + /** + * Checks if can navigate back + * + * @param result + */ + private void canGoBack(MethodChannel.Result result) { + if (webViewManager != null) { + result.success(webViewManager.canGoBack()); + } else { + result.error("Webview is null", null, null); + } + } + + /** + * Navigates back on the Webview. + */ + private void back(MethodCall call, MethodChannel.Result result) { + if (webViewManager != null) { + webViewManager.back(call, result); + } + result.success(null); + } + + /** + * Checks if can navigate forward + * @param result + */ + private void canGoForward(MethodChannel.Result result) { + if (webViewManager != null) { + result.success(webViewManager.canGoForward()); + } else { + result.error("Webview is null", null, null); + } + } + + /** + * Navigates forward on the Webview. + */ + private void forward(MethodCall call, MethodChannel.Result result) { + if (webViewManager != null) { + webViewManager.forward(call, result); + } + result.success(null); + } + + /** + * Reloads the Webview. + */ + private void reload(MethodCall call, MethodChannel.Result result) { + if (webViewManager != null) { + webViewManager.reload(call, result); + } + result.success(null); + } + + private void reloadUrl(MethodCall call, MethodChannel.Result result) { + if (webViewManager != null) { + String url = call.argument("url"); + Map headers = call.argument("headers"); + if (headers != null) { + webViewManager.reloadUrl(url, headers); + } else { + webViewManager.reloadUrl(url); + } + + } + result.success(null); + } + + private void eval(MethodCall call, final MethodChannel.Result result) { + if (webViewManager != null) { + webViewManager.eval(call, result); + } + } + + private void resize(MethodCall call, final MethodChannel.Result result) { + if (webViewManager != null) { + FrameLayout.LayoutParams params = buildLayoutParams(call); + webViewManager.resize(params); + } + result.success(null); + } + + private void hide(MethodCall call, final MethodChannel.Result result) { + if (webViewManager != null) { + webViewManager.hide(call, result); + } + result.success(null); + } + + private void show(MethodCall call, final MethodChannel.Result result) { + if (webViewManager != null) { + webViewManager.show(call, result); + } + result.success(null); + } + + private void cleanCookies(MethodCall call, final MethodChannel.Result result) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().removeAllCookies(new ValueCallback() { + @Override + public void onReceiveValue(Boolean aBoolean) { + + } + }); + } else { + CookieManager.getInstance().removeAllCookie(); + } + result.success(null); + } + + private int dp2px(Context context, float dp) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dp * scale + 0.5f); + } + + @Override + public boolean onActivityResult(int i, int i1, Intent intent) { + if (webViewManager != null && webViewManager.resultHandler != null) { + return webViewManager.resultHandler.handleResult(i, i1, intent); + } + return false; + } } diff --git a/android/src/main/java/com/flutter_webview_plugin/JavaScriptChannel.java b/android/src/main/java/com/flutter_webview_plugin/JavaScriptChannel.java new file mode 100755 index 00000000..86c82a64 --- /dev/null +++ b/android/src/main/java/com/flutter_webview_plugin/JavaScriptChannel.java @@ -0,0 +1,60 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package com.flutter_webview_plugin; + +import android.os.Handler; +import android.os.Looper; +import android.webkit.JavascriptInterface; + +import java.util.HashMap; + +import io.flutter.plugin.common.MethodChannel; + +/** + * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets + * up. + * + *

Exposes a single method named `postMessage` to JavaScript, which sends a message over a method + * channel to the Dart code. + */ +class JavaScriptChannel { + private final MethodChannel methodChannel; + private final String javaScriptChannelName; + private final Handler platformThreadHandler; + + /** + * @param methodChannel the Flutter WebView method channel to which JS messages are sent + * @param javaScriptChannelName the name of the JavaScript channel, this is sent over the method + * channel with each message to let the Dart code know which JavaScript channel the message + * was sent through + */ + JavaScriptChannel( + MethodChannel methodChannel, String javaScriptChannelName, Handler platformThreadHandler) { + this.methodChannel = methodChannel; + this.javaScriptChannelName = javaScriptChannelName; + this.platformThreadHandler = platformThreadHandler; + } + + // Suppressing unused warning as this is invoked from JavaScript. + @SuppressWarnings("unused") + @JavascriptInterface + public void postMessage(final String message) { + Runnable postMessageRunnable = + new Runnable() { + @Override + public void run() { + HashMap arguments = new HashMap<>(); + arguments.put("channel", javaScriptChannelName); + arguments.put("message", message); + methodChannel.invokeMethod("javascriptChannelMessage", arguments); + } + }; + if (platformThreadHandler.getLooper() == Looper.myLooper()) { + postMessageRunnable.run(); + } else { + platformThreadHandler.post(postMessageRunnable); + } + } +} diff --git a/android/src/main/java/com/flutter_webview_plugin/ObservableWebView.java b/android/src/main/java/com/flutter_webview_plugin/ObservableWebView.java new file mode 100644 index 00000000..2c02f61f --- /dev/null +++ b/android/src/main/java/com/flutter_webview_plugin/ObservableWebView.java @@ -0,0 +1,49 @@ +package com.flutter_webview_plugin; + +import android.content.Context; +import android.util.AttributeSet; +import android.webkit.WebView; + +public class ObservableWebView extends WebView { + private OnScrollChangedCallback mOnScrollChangedCallback; + + public ObservableWebView(final Context context) + { + super(context); + } + + public ObservableWebView(final Context context, final AttributeSet attrs) + { + super(context, attrs); + } + + public ObservableWebView(final Context context, final AttributeSet attrs, final int defStyle) + { + super(context, attrs, defStyle); + } + + @Override + protected void onScrollChanged(final int l, final int t, final int oldl, final int oldt) + { + super.onScrollChanged(l, t, oldl, oldt); + if(mOnScrollChangedCallback != null) mOnScrollChangedCallback.onScroll(l, t, oldl, oldt); + } + + public OnScrollChangedCallback getOnScrollChangedCallback() + { + return mOnScrollChangedCallback; + } + + public void setOnScrollChangedCallback(final OnScrollChangedCallback onScrollChangedCallback) + { + mOnScrollChangedCallback = onScrollChangedCallback; + } + + /** + * Impliment in the activity/fragment/view that you want to listen to the webview + */ + public static interface OnScrollChangedCallback + { + public void onScroll(int l, int t, int oldl, int oldt); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/flutter_webview_plugin/WebviewActivity.java b/android/src/main/java/com/flutter_webview_plugin/WebviewActivity.java deleted file mode 100644 index 4ec69393..00000000 --- a/android/src/main/java/com/flutter_webview_plugin/WebviewActivity.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.flutter_webview_plugin; - -import android.app.Activity; -import android.graphics.Bitmap; -import android.os.Build; -import android.os.Bundle; -import android.webkit.CookieManager; -import android.webkit.ValueCallback; -import android.webkit.WebView; -import android.webkit.WebViewClient; - -import java.util.HashMap; -import java.util.Map; - -/** - * Created by lejard_h on 23/04/2017. - */ - -public class WebviewActivity extends Activity { - - static public final String URL_KEY = "URL"; - static public final String CLEAR_CACHE_KEY = "CLEAR_CACHE"; - static public final String CLEAR_COOKIES_KEY = "CLEAR_COOKIES"; - static public final String WITH_JAVASCRIPT_KEY = "WITH_JAVASCRIPT"; - - private WebView webView; - - public WebviewActivity() { - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - webView = initWebview(); - setContentView(webView); - clearCookies(); - clearCache(); - setWebViewClient(); - loadUrl(); - } - - protected WebView initWebview() { - return new WebView(this); - } - - protected void clearCookies() { - if (getIntent().getBooleanExtra(CLEAR_COOKIES_KEY, false)) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - CookieManager.getInstance().removeAllCookies(new ValueCallback() { - @Override - public void onReceiveValue(Boolean aBoolean) { - - } - }); - } else { - CookieManager.getInstance().removeAllCookie(); - } - } - } - - protected void clearCache() { - if (getIntent().getBooleanExtra(CLEAR_CACHE_KEY, false)) { - webView.clearCache(true); - webView.clearFormData(); - } - } - - protected WebViewClient setWebViewClient() { - WebViewClient webViewClient = new BrowserClient(); - webView.setWebViewClient(webViewClient); - return webViewClient; - } - - protected void loadUrl() { - webView.getSettings().setJavaScriptEnabled(getIntent().getBooleanExtra(WITH_JAVASCRIPT_KEY, true)); - webView.loadUrl(getIntent().getStringExtra(URL_KEY)); - } - - @Override - protected void onDestroy() { - FlutterWebviewPlugin.channel.invokeMethod("onDestroy", null); - super.onDestroy(); - } - - @Override - public void onBackPressed() { - if(webView.canGoBack()){ - webView.goBack(); - return; - } - FlutterWebviewPlugin.channel.invokeMethod("onBackPressed", null); - super.onBackPressed(); - } - - - private class BrowserClient extends WebViewClient { - private BrowserClient() { - super(); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - super.onPageStarted(view, url, favicon); - Map data = new HashMap<>(); - data.put("url", url); - FlutterWebviewPlugin.channel.invokeMethod("onUrlChanged", data); - } - } -} \ No newline at end of file diff --git a/android/src/main/java/com/flutter_webview_plugin/WebviewManager.java b/android/src/main/java/com/flutter_webview_plugin/WebviewManager.java new file mode 100644 index 00000000..51190104 --- /dev/null +++ b/android/src/main/java/com/flutter_webview_plugin/WebviewManager.java @@ -0,0 +1,547 @@ +package com.flutter_webview_plugin; + +import android.content.Intent; +import android.net.Uri; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.CookieManager; +import android.webkit.GeolocationPermissions; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.widget.FrameLayout; +import android.provider.MediaStore; + +import androidx.core.content.FileProvider; + +import android.database.Cursor; +import android.provider.OpenableColumns; + +import java.util.ArrayList; +import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.io.File; +import java.util.Date; +import java.io.IOException; +import java.text.SimpleDateFormat; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +import static android.app.Activity.RESULT_OK; + +/** + * Created by lejard_h on 20/12/2017. + */ + +class WebviewManager { + + private ValueCallback mUploadMessage; + private ValueCallback mUploadMessageArray; + private final static int FILECHOOSER_RESULTCODE = 1; + private Uri fileUri; + private Uri videoUri; + + private long getFileSize(Uri fileUri) { + Cursor returnCursor = context.getContentResolver().query(fileUri, null, null, null, null); + returnCursor.moveToFirst(); + int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE); + return returnCursor.getLong(sizeIndex); + } + + @TargetApi(7) + class ResultHandler { + public boolean handleResult(int requestCode, int resultCode, Intent intent) { + boolean handled = false; + if (Build.VERSION.SDK_INT >= 21) { + if (requestCode == FILECHOOSER_RESULTCODE) { + Uri[] results = null; + if (resultCode == Activity.RESULT_OK) { + if (fileUri != null && getFileSize(fileUri) > 0) { + results = new Uri[]{fileUri}; + } else if (videoUri != null && getFileSize(videoUri) > 0) { + results = new Uri[]{videoUri}; + } else if (intent != null) { + results = getSelectedFiles(intent); + } + } + if (mUploadMessageArray != null) { + mUploadMessageArray.onReceiveValue(results); + mUploadMessageArray = null; + } + handled = true; + } + } else { + if (requestCode == FILECHOOSER_RESULTCODE) { + Uri result = null; + if (resultCode == RESULT_OK && intent != null) { + result = intent.getData(); + } + if (mUploadMessage != null) { + mUploadMessage.onReceiveValue(result); + mUploadMessage = null; + } + handled = true; + } + } + return handled; + } + } + + private Uri[] getSelectedFiles(Intent data) { + // we have one files selected + if (data.getData() != null) { + String dataString = data.getDataString(); + if (dataString != null) { + return new Uri[]{Uri.parse(dataString)}; + } + } + // we have multiple files selected + if (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; + } + return null; + } + + private final Handler platformThreadHandler; + boolean closed = false; + WebView webView; + Activity activity; + BrowserClient webViewClient; + ResultHandler resultHandler; + Context context; + + WebviewManager(final Activity activity, final Context context, final List channelNames) { + this.webView = new ObservableWebView(activity); + this.activity = activity; + this.context = context; + this.resultHandler = new ResultHandler(); + this.platformThreadHandler = new Handler(context.getMainLooper()); + webViewClient = new BrowserClient(); + webView.setOnKeyListener(new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + if (webView.canGoBack()) { + webView.goBack(); + } else { + FlutterWebviewPlugin.channel.invokeMethod("onBack", null); + } + return true; + } + } + + return false; + } + }); + + ((ObservableWebView) webView).setOnScrollChangedCallback(new ObservableWebView.OnScrollChangedCallback() { + public void onScroll(int x, int y, int oldx, int oldy) { + Map yDirection = new HashMap<>(); + yDirection.put("yDirection", (double) y); + FlutterWebviewPlugin.channel.invokeMethod("onScrollYChanged", yDirection); + Map xDirection = new HashMap<>(); + xDirection.put("xDirection", (double) x); + FlutterWebviewPlugin.channel.invokeMethod("onScrollXChanged", xDirection); + } + }); + + webView.setWebViewClient(webViewClient); + webView.setWebChromeClient(new WebChromeClient() { + //The undocumented magic method override + //Eclipse will swear at you if you try to put @Override here + // For Android 3.0+ + public void openFileChooser(ValueCallback uploadMsg) { + + mUploadMessage = uploadMsg; + Intent i = new Intent(Intent.ACTION_GET_CONTENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType("image/*"); + activity.startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE); + + } + + // For Android 3.0+ + public void openFileChooser(ValueCallback uploadMsg, String acceptType) { + mUploadMessage = uploadMsg; + Intent i = new Intent(Intent.ACTION_GET_CONTENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType("*/*"); + activity.startActivityForResult( + Intent.createChooser(i, "File Browser"), + FILECHOOSER_RESULTCODE); + } + + //For Android 4.1 + public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture) { + mUploadMessage = uploadMsg; + Intent i = new Intent(Intent.ACTION_GET_CONTENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType("image/*"); + activity.startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE); + + } + + //For Android 5.0+ + public boolean onShowFileChooser( + WebView webView, ValueCallback filePathCallback, + FileChooserParams fileChooserParams) { + if (mUploadMessageArray != null) { + mUploadMessageArray.onReceiveValue(null); + } + mUploadMessageArray = filePathCallback; + + final String[] acceptTypes = getSafeAcceptedTypes(fileChooserParams); + List intentList = new ArrayList(); + fileUri = null; + videoUri = null; + if (acceptsImages(acceptTypes)) { + Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + fileUri = getOutputFilename(MediaStore.ACTION_IMAGE_CAPTURE); + takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); + intentList.add(takePhotoIntent); + } + if (acceptsVideo(acceptTypes)) { + Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + videoUri = getOutputFilename(MediaStore.ACTION_VIDEO_CAPTURE); + takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri); + intentList.add(takeVideoIntent); + } + Intent contentSelectionIntent; + if (Build.VERSION.SDK_INT >= 21) { + final boolean allowMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE; + contentSelectionIntent = fileChooserParams.createIntent(); + contentSelectionIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); + } else { + contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT); + contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE); + contentSelectionIntent.setType("*/*"); + } + Intent[] intentArray = intentList.toArray(new Intent[intentList.size()]); + + Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); + chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent); + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray); + activity.startActivityForResult(chooserIntent, FILECHOOSER_RESULTCODE); + return true; + } + + @Override + public void onProgressChanged(WebView view, int progress) { + Map args = new HashMap<>(); + args.put("progress", progress / 100.0); + FlutterWebviewPlugin.channel.invokeMethod("onProgressChanged", args); + } + + public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { + callback.invoke(origin, true, false); + } + }); + registerJavaScriptChannelNames(channelNames); + } + + private Uri getOutputFilename(String intentType) { + String prefix = ""; + String suffix = ""; + + if (intentType == MediaStore.ACTION_IMAGE_CAPTURE) { + prefix = "image-"; + suffix = ".jpg"; + } else if (intentType == MediaStore.ACTION_VIDEO_CAPTURE) { + prefix = "video-"; + suffix = ".mp4"; + } + + String packageName = context.getPackageName(); + File capturedFile = null; + try { + capturedFile = createCapturedFile(prefix, suffix); + } catch (IOException e) { + e.printStackTrace(); + } + return FileProvider.getUriForFile(context, packageName + ".fileprovider", capturedFile); + } + + private File createCapturedFile(String prefix, String suffix) throws IOException { + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); + String imageFileName = prefix + "_" + timeStamp; + File storageDir = context.getExternalFilesDir(null); + return File.createTempFile(imageFileName, suffix, storageDir); + } + + private Boolean acceptsImages(String[] types) { + return isArrayEmpty(types) || arrayContainsString(types, "image"); + } + + private Boolean acceptsVideo(String[] types) { + return isArrayEmpty(types) || arrayContainsString(types, "video"); + } + + private Boolean arrayContainsString(String[] array, String pattern) { + for (String content : array) { + if (content.contains(pattern)) { + return true; + } + } + return false; + } + + 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); + } + + private String[] getSafeAcceptedTypes(WebChromeClient.FileChooserParams params) { + + // the getAcceptTypes() is available only in api 21+ + // for lower level, we ignore it + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return params.getAcceptTypes(); + } + + final String[] EMPTY = {}; + return EMPTY; + } + + 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(); + } + } + + private void setCookies(String url, ArrayList cookies) { + for (String cookie : cookies) { + CookieManager.getInstance().setCookie(url, cookie); + } + } + + private void clearCache() { + webView.clearCache(true); + webView.clearFormData(); + } + + private void registerJavaScriptChannelNames(List channelNames) { + for (String channelName : channelNames) { + webView.addJavascriptInterface( + new JavaScriptChannel(FlutterWebviewPlugin.channel, channelName, platformThreadHandler), channelName); + } + } + + void openUrl( + boolean withJavascript, + boolean clearCache, + boolean hidden, + boolean clearCookies, + ArrayList cookies, + String userAgent, + String url, + Map headers, + boolean withZoom, + boolean displayZoomControls, + boolean withLocalStorage, + boolean withOverviewMode, + boolean scrollBar, + boolean supportMultipleWindows, + boolean appCacheEnabled, + boolean allowFileURLs, + boolean useWideViewPort, + String invalidUrlRegex, + boolean geolocationEnabled, + boolean debuggingEnabled + ) { + webView.getSettings().setJavaScriptEnabled(withJavascript); + webView.getSettings().setBuiltInZoomControls(withZoom); + webView.getSettings().setSupportZoom(withZoom); + webView.getSettings().setDisplayZoomControls(displayZoomControls); + webView.getSettings().setDomStorageEnabled(withLocalStorage); + webView.getSettings().setLoadWithOverviewMode(withOverviewMode); + webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(supportMultipleWindows); + + webView.getSettings().setSupportMultipleWindows(supportMultipleWindows); + + webView.getSettings().setAppCacheEnabled(appCacheEnabled); + + webView.getSettings().setAllowFileAccessFromFileURLs(allowFileURLs); + webView.getSettings().setAllowUniversalAccessFromFileURLs(allowFileURLs); + + webView.getSettings().setUseWideViewPort(useWideViewPort); + + // Handle debugging + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.setWebContentsDebuggingEnabled(debuggingEnabled); + } + + webViewClient.updateInvalidUrlRegex(invalidUrlRegex); + + if (geolocationEnabled) { + webView.getSettings().setGeolocationEnabled(true); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); + } + + if (clearCache) { + clearCache(); + } + + if (hidden) { + webView.setVisibility(View.GONE); + } + + if (clearCookies) { + clearCookies(); + } + + if (cookies != null && !cookies.isEmpty()) { + setCookies(url, cookies); + } + + if (userAgent != null) { + webView.getSettings().setUserAgentString(userAgent); + } + + if (!scrollBar) { + webView.setVerticalScrollBarEnabled(false); + } + + if (headers != null) { + webView.loadUrl(url, headers); + } else { + webView.loadUrl(url); + } + } + + void reloadUrl(String url) { + webView.loadUrl(url); + } + + void reloadUrl(String url, Map headers) { + webView.loadUrl(url, headers); + } + + void close(MethodCall call, MethodChannel.Result result) { + if (webView != null) { + ViewGroup vg = (ViewGroup) (webView.getParent()); + vg.removeView(webView); + } + webView = null; + if (result != null) { + result.success(null); + } + + closed = true; + FlutterWebviewPlugin.channel.invokeMethod("onDestroy", null); + } + + void close() { + close(null, null); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + void eval(MethodCall call, final MethodChannel.Result result) { + String code = call.argument("code"); + + webView.evaluateJavascript(code, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } + + /** + * Reloads the Webview. + */ + void reload(MethodCall call, MethodChannel.Result result) { + if (webView != null) { + webView.reload(); + } + } + + /** + * Navigates back on the Webview. + */ + void back(MethodCall call, MethodChannel.Result result) { + if (webView != null && webView.canGoBack()) { + webView.goBack(); + } + } + + /** + * Navigates forward on the Webview. + */ + void forward(MethodCall call, MethodChannel.Result result) { + if (webView != null && webView.canGoForward()) { + webView.goForward(); + } + } + + void resize(FrameLayout.LayoutParams params) { + webView.setLayoutParams(params); + } + + /** + * Checks if going back on the Webview is possible. + */ + boolean canGoBack() { + return webView.canGoBack(); + } + + /** + * Checks if going forward on the Webview is possible. + */ + boolean canGoForward() { + return webView.canGoForward(); + } + + /** + * Clears cache + */ + void cleanCache(){ + webView.clearCache(true); + } + + void hide(MethodCall call, MethodChannel.Result result) { + if (webView != null) { + webView.setVisibility(View.GONE); + } + } + + void show(MethodCall call, MethodChannel.Result result) { + if (webView != null) { + webView.setVisibility(View.VISIBLE); + } + } + + void stopLoading(MethodCall call, MethodChannel.Result result) { + if (webView != null) { + webView.stopLoading(); + } + } +} diff --git a/android/src/main/res/xml/filepaths.xml b/android/src/main/res/xml/filepaths.xml new file mode 100644 index 00000000..43f25539 --- /dev/null +++ b/android/src/main/res/xml/filepaths.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/android/src/test/java/com/flutter_webview_plugin/FlutterWebviewPluginTest.java b/android/src/test/java/com/flutter_webview_plugin/FlutterWebviewPluginTest.java new file mode 100644 index 00000000..8feb0584 --- /dev/null +++ b/android/src/test/java/com/flutter_webview_plugin/FlutterWebviewPluginTest.java @@ -0,0 +1,41 @@ +package com.flutter_webview_plugin; + +import android.app.Activity; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import io.flutter.plugin.common.ErrorLogResult; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +import static org.mockito.Mockito.verify; + +public class FlutterWebviewPluginTest { + + @Mock + Activity mockActivity; + + MethodCall mockMethodCall; + MethodChannel.Result mockResult; + + @Spy + FlutterWebviewPlugin flutterWebviewPlugin = new FlutterWebviewPlugin(mockActivity, mockActivity); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void shouldInvokeClose() { + mockMethodCall = new MethodCall("close", null); + mockResult = new ErrorLogResult(""); + flutterWebviewPlugin.onMethodCall(mockMethodCall, mockResult); + verify(flutterWebviewPlugin).close(mockMethodCall, mockResult); + + } +} \ No newline at end of file diff --git a/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..ca6ee9ce --- /dev/null +++ b/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/android_test.sh b/android_test.sh new file mode 100755 index 00000000..89c75823 --- /dev/null +++ b/android_test.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd example/android +./gradlew test -DflutterPath=$FLUTTER_HOME diff --git a/example/android/.gitignore b/example/android/.gitignore index e6a9f067..b985fd38 100644 --- a/example/android/.gitignore +++ b/example/android/.gitignore @@ -8,6 +8,5 @@ /captures PluginRegistry.java -/gradle -/gradlew -/gradlew.bat +.project +.settings diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 61ecf403..c1418a3d 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -15,8 +15,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 25 - buildToolsVersion '25.0.2' + compileSdkVersion 28 lintOptions { disable 'InvalidPackage' @@ -24,9 +23,9 @@ android { defaultConfig { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.yourcompany.flutter_webview_plugin_example" + minSdkVersion 16 } buildTypes { @@ -43,7 +42,9 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' - androidTestCompile 'com.android.support.test:runner:0.5' - androidTestCompile 'com.android.support.test:rules:0.5' + testImplementation 'junit:junit:4.12' + + androidTestImplementation 'androidx.annotation:annotation:1.0.2' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index d7e68ac7..981cfe8c 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -3,8 +3,6 @@ android:versionCode="1" android:versionName="0.0.1"> - -