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/.gitignore b/.gitignore index 731501dd..77e266aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store .atom/ .idea +.vscode .packages .pub/ build/ @@ -9,3 +10,8 @@ packages pubspec.lock example/ios/Podfile.lock +**/Flutter/App.framework/ +**/Flutter/Flutter.framework/ +**/Flutter/Generated.xcconfig/ +**/Flutter/flutter_assets/ + diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f348c4..639c7b48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,83 @@ +# 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..6626d3f2 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ -# flutter_webview_plugin + -Plugin that allow Flutter to communicate with a native WebView. +[Flutter Community](https://github.com/fluttercommunity/community) -***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*** +# 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*** +[![pub package](https://img.shields.io/pub/v/flutter_webview_plugin.svg)](https://pub.dartlang.org/packages/flutter_webview_plugin) - - [x] Android - - [x] IOS +Plugin that allows Flutter to communicate with a native WebView. + +**_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 to use snackbars, dialogs ... ## Getting Started @@ -15,44 +18,192 @@ For help getting started with Flutter, view our online [documentation](http://fl ### How it works -#### Launch WebView with variable url +#### Launch WebView Fullscreen with Flutter navigation + +```dart +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(); +``` + +#### Webview inside custom Rectangle + +```dart +final flutterWebviewPlugin = new FlutterWebviewPlugin(); + +flutterWebviewPlugin.launch(url, + fullScreen: false, + rect: new Rect.fromLTWH( + 0.0, + 0.0, + MediaQuery.of(context).size.width, + 300.0, + ), +); +``` + +### 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 -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; -} +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, + bool scrollBar: true, + bool supportMultipleWindows: false, + bool appCacheEnabled: false, + bool allowFileURLs: false, +}); ``` -### Close launched WebView +```dart +Future evalJavascript(String code); +``` ```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(); - }); -} +Future> getCookies(); ``` -### Android +```dart +Future cleanCookies(); +``` + +```dart +Future resize(Rect rect); +``` -Add the Activity to you `AndroidManifest.xml`: +```dart +Future show(); +``` -```xml - +```dart +Future hide(); ``` -### iOS +```dart +Future reloadUrl(String url); +``` + +```dart +Future close(); +``` + +```dart +Future reload(); +``` + +```dart +Future goBack(); +``` + +```dart +Future goForward(); +``` + +```dart +Future stopLoading(); +``` -No extra configuration is needed. 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/build.gradle b/android/build.gradle index b308a6cd..fcf5027f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,13 +1,14 @@ -group 'com.yourcompany.flutter_webview_plugin' +group 'com.flutter_webview_plugin' version '1.0-SNAPSHOT' buildscript { repositories { + google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:3.2.1' } } @@ -20,11 +21,13 @@ allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 25 - buildToolsVersion '25.0.0' + compileSdkVersion 27 defaultConfig { testInstrumentationRunner "android.support.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' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index ec6979f3..b3b8fc6b 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,7 +1,3 @@ - - + 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..55583883 --- /dev/null +++ b/android/src/main/java/com/flutter_webview_plugin/BrowserClient.java @@ -0,0 +1,51 @@ +package com.flutter_webview_plugin; + +import android.graphics.Bitmap; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by lejard_h on 20/12/2017. + */ + +public class BrowserClient extends WebViewClient { + public 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); + 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); + + } + + @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); + } +} \ 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..a9433516 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,18 @@ 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.widget.FrameLayout; +import android.webkit.CookieManager; +import android.webkit.ValueCallback; +import android.os.Build; + +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 +21,234 @@ /** * 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; + static MethodChannel channel; + private static final String CHANNEL_NAME = "flutter_webview_plugin"; + + public static void registerWith(PluginRegistry.Registrar registrar) { + channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME); + final FlutterWebviewPlugin instance = new FlutterWebviewPlugin(registrar.activity()); + registrar.addActivityResultListener(instance); + 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; + 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; + default: + result.notImplemented(); + break; + } + } + + private 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"); + boolean withZoom = call.argument("withZoom"); + boolean withLocalStorage = call.argument("withLocalStorage"); + 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 geolocationEnabled = call.argument("geolocationEnabled"); + + if (webViewManager == null || webViewManager.closed == true) { + webViewManager = new WebviewManager(activity); + } + + FrameLayout.LayoutParams params = buildLayoutParams(call); + + activity.addContentView(webViewManager.webView, params); + + webViewManager.openUrl(withJavascript, + clearCache, + hidden, + clearCookies, + userAgent, + url, + headers, + withZoom, + withLocalStorage, + scrollBar, + supportMultipleWindows, + appCacheEnabled, + allowFileURLs, + geolocationEnabled + ); + 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); + } + + private void close(MethodCall call, MethodChannel.Result result) { + if (webViewManager != null) { + webViewManager.close(call, result); + webViewManager = null; + } + } + + /** + * Navigates back on the Webview. + */ + private void back(MethodCall call, MethodChannel.Result result) { + if (webViewManager != null) { + webViewManager.back(call, result); + } + result.success(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"); + 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/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..756b6027 --- /dev/null +++ b/android/src/main/java/com/flutter_webview_plugin/WebviewManager.java @@ -0,0 +1,354 @@ +package com.flutter_webview_plugin; + +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.annotation.TargetApi; +import android.app.Activity; +import android.os.Build; +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.webkit.WebViewClient; +import android.widget.FrameLayout; + +import java.util.HashMap; +import java.util.Map; + +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; + + @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 && intent != null){ + String dataString = intent.getDataString(); + if(dataString != null){ + results = new Uri[]{ Uri.parse(dataString) }; + } + } + 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; + } + } + + boolean closed = false; + WebView webView; + Activity activity; + ResultHandler resultHandler; + + WebviewManager(final Activity activity) { + this.webView = new ObservableWebView(activity); + this.activity = activity; + this.resultHandler = new ResultHandler(); + WebViewClient 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 { + close(); + } + 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; + + Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT); + contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE); + contentSelectionIntent.setType("*/*"); + Intent[] intentArray; + intentArray = new Intent[0]; + + Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); + chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent); + chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser"); + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray); + activity.startActivityForResult(chooserIntent, FILECHOOSER_RESULTCODE); + return true; + } + }); + } + + 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 clearCache() { + webView.clearCache(true); + webView.clearFormData(); + } + + void openUrl( + boolean withJavascript, + boolean clearCache, + boolean hidden, + boolean clearCookies, + String userAgent, + String url, + Map headers, + boolean withZoom, + boolean withLocalStorage, + boolean scrollBar, + boolean supportMultipleWindows, + boolean appCacheEnabled, + boolean allowFileURLs, + boolean geolocationEnabled + ) { + webView.getSettings().setJavaScriptEnabled(withJavascript); + webView.getSettings().setBuiltInZoomControls(withZoom); + webView.getSettings().setSupportZoom(withZoom); + webView.getSettings().setDomStorageEnabled(withLocalStorage); + webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(supportMultipleWindows); + + webView.getSettings().setSupportMultipleWindows(supportMultipleWindows); + + webView.getSettings().setAppCacheEnabled(appCacheEnabled); + + webView.getSettings().setAllowFileAccessFromFileURLs(allowFileURLs); + webView.getSettings().setAllowUniversalAccessFromFileURLs(allowFileURLs); + + if (geolocationEnabled) { + webView.getSettings().setGeolocationEnabled(true); + webView.setWebChromeClient(new WebChromeClient() { + @Override + public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { + callback.invoke(origin, true, false); + } + }); + } + + 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 (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 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(); + } + 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/example/android/app/build.gradle b/example/android/app/build.gradle index 61ecf403..8d163b0d 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 27 lintOptions { disable 'InvalidPackage' @@ -24,7 +23,6 @@ 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" } @@ -43,7 +41,7 @@ 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' + androidTestImplementation 'com.android.support:support-annotations:27.0.0' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test:rules:1.0.2' } 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"> - -