From dc0f9c687db34ed8b49a8cd34d3c003c6c505d1e Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 7 Dec 2021 10:09:05 +0100 Subject: [PATCH] [webview_flutter] Android implementation of `loadFlutterAsset` method. (#4581) --- .../webview_flutter_android/CHANGELOG.md | 4 + .../android/build.gradle | 4 - .../webviewflutter/FlutterAssetManager.java | 108 ++++++++++++++++++ .../FlutterAssetManagerHostApiImpl.java | 46 ++++++++ .../GeneratedAndroidWebView.java | 79 +++++++++++++ .../webviewflutter/WebViewFlutterPlugin.java | 14 ++- .../FlutterAssetManagerHostApiImplTest.java | 76 ++++++++++++ .../PluginBindingFlutterAssetManagerTest.java | 54 +++++++++ .../RegistrarFlutterAssetManagerTest.java | 55 +++++++++ .../example/assets/www/index.html | 20 ++++ .../example/assets/www/styles/style.css | 3 + .../example/lib/main.dart | 13 +++ .../example/lib/web_view.dart | 8 ++ .../example/pubspec.yaml | 2 + .../lib/src/android_webview.dart | 21 ++++ .../lib/src/android_webview.pigeon.dart | 67 +++++++++++ .../lib/webview_android_widget.dart | 38 +++++- .../pigeons/android_webview.dart | 7 ++ .../webview_flutter_android/pubspec.yaml | 4 +- .../test/android_webview.pigeon.dart | 53 +++++++++ .../test/android_webview_test.dart | 1 + .../test/android_webview_test.mocks.dart | 21 ++++ .../test/webview_android_widget_test.dart | 65 +++++++++++ .../webview_android_widget_test.mocks.dart | 22 ++++ 24 files changed, 775 insertions(+), 10 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/example/assets/www/index.html create mode 100644 packages/webview_flutter/webview_flutter_android/example/assets/www/styles/style.css diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index caee5f7cb75a..e8d9e639c59d 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.0 + +* Adds implementation of the `loadFlutterAsset` method from the platform interface. + ## 2.5.0 * Adds an option to set the background color of the webview. diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle index e70d4e68edc8..37954b36a834 100644 --- a/packages/webview_flutter/webview_flutter_android/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -58,8 +58,4 @@ android { } } } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java new file mode 100644 index 000000000000..1d484d8639a0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java @@ -0,0 +1,108 @@ +// Copyright 2013 The Flutter 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 io.flutter.plugins.webviewflutter; + +import android.content.res.AssetManager; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.PluginRegistry; +import java.io.IOException; + +/** Provides access to the assets registered as part of the App bundle. */ +abstract class FlutterAssetManager { + final AssetManager assetManager; + + /** + * Constructs a new instance of the {@link FlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within the + * App bundle. + */ + public FlutterAssetManager(AssetManager assetManager) { + this.assetManager = assetManager; + } + + /** + * Gets the relative file path to the Flutter asset with the given name, including the file's + * extension, e.g., "myImage.jpg". + * + *

The returned file path is relative to the Android app's standard asset's directory. + * Therefore, the returned path is appropriate to pass to Android's AssetManager, but the path is + * not appropriate to load as an absolute path. + */ + abstract String getAssetFilePathByName(String name); + + /** + * Returns a String array of all the assets at the given path. + * + * @param path A relative path within the assets, i.e., "docs/home.html". This value cannot be + * null. + * @return String[] Array of strings, one for each asset. These file names are relative to 'path'. + * This value may be null. + * @throws IOException Throws an IOException in case I/O operations were interrupted. + */ + public String[] list(@NonNull String path) throws IOException { + return assetManager.list(path); + } + + /** + * Provides access to assets using the {@link PluginRegistry.Registrar} for looking up file paths + * to Flutter assets. + * + * @deprecated The {@link RegistrarFlutterAssetManager} is for Flutter's v1 embedding. For + * instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + */ + @Deprecated + static class RegistrarFlutterAssetManager extends FlutterAssetManager { + final PluginRegistry.Registrar registrar; + + /** + * Constructs a new instance of the {@link RegistrarFlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within + * the App bundle. + * @param registrar Instance of {@link io.flutter.plugin.common.PluginRegistry.Registrar} used + * to look up file paths to assets registered by Flutter. + */ + RegistrarFlutterAssetManager(AssetManager assetManager, PluginRegistry.Registrar registrar) { + super(assetManager); + this.registrar = registrar; + } + + @Override + public String getAssetFilePathByName(String name) { + return registrar.lookupKeyForAsset(name); + } + } + + /** + * Provides access to assets using the {@link FlutterPlugin.FlutterAssets} for looking up file + * paths to Flutter assets. + */ + static class PluginBindingFlutterAssetManager extends FlutterAssetManager { + final FlutterPlugin.FlutterAssets flutterAssets; + + /** + * Constructs a new instance of the {@link PluginBindingFlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within + * the App bundle. + * @param flutterAssets Instance of {@link + * io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterAssets} used to look up file + * paths to assets registered by Flutter. + */ + PluginBindingFlutterAssetManager( + AssetManager assetManager, FlutterPlugin.FlutterAssets flutterAssets) { + super(assetManager); + this.flutterAssets = flutterAssets; + } + + @Override + public String getAssetFilePathByName(String name) { + return flutterAssets.getAssetFilePathByName(name); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java new file mode 100644 index 000000000000..791912adb815 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter 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 io.flutter.plugins.webviewflutter; + +import android.webkit.WebView; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Host api implementation for {@link WebView}. + * + *

Handles creating {@link WebView}s that intercommunicate with a paired Dart object. + */ +public class FlutterAssetManagerHostApiImpl implements FlutterAssetManagerHostApi { + final FlutterAssetManager flutterAssetManager; + + /** Constructs a new instance of {@link FlutterAssetManagerHostApiImpl}. */ + public FlutterAssetManagerHostApiImpl(FlutterAssetManager flutterAssetManager) { + this.flutterAssetManager = flutterAssetManager; + } + + @Override + public List list(String path) { + try { + String[] paths = flutterAssetManager.list(path); + + if (paths == null) { + return new ArrayList<>(); + } + + return Arrays.asList(paths); + } catch (IOException ex) { + throw new RuntimeException(ex.getMessage()); + } + } + + @Override + public String getAssetFilePathByName(String name) { + return flutterAssetManager.getAssetFilePathByName(name); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index a5632d351f83..0123790ee799 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; /** Generated class from Pigeon. */ @@ -1915,6 +1916,84 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { } } + private static class FlutterAssetManagerHostApiCodec extends StandardMessageCodec { + public static final FlutterAssetManagerHostApiCodec INSTANCE = + new FlutterAssetManagerHostApiCodec(); + + private FlutterAssetManagerHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface FlutterAssetManagerHostApi { + List list(String path); + + String getAssetFilePathByName(String name); + + /** The codec used by FlutterAssetManagerHostApi. */ + static MessageCodec getCodec() { + return FlutterAssetManagerHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `FlutterAssetManagerHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FlutterAssetManagerHostApi.list", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String pathArg = (String) args.get(0); + if (pathArg == null) { + throw new NullPointerException("pathArg unexpectedly null."); + } + List output = api.list(pathArg); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String nameArg = (String) args.get(0); + if (nameArg == null) { + throw new NullPointerException("nameArg unexpectedly null."); + } + String output = api.getAssetFilePathByName(nameArg); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static class WebChromeClientFlutterApiCodec extends StandardMessageCodec { public static final WebChromeClientFlutterApiCodec INSTANCE = new WebChromeClientFlutterApiCodec(); diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 2b174ff35e6f..cbeda8dcf493 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -14,6 +14,7 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.platform.PlatformViewRegistry; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi; @@ -61,7 +62,9 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra registrar.messenger(), registrar.platformViewRegistry(), registrar.activity(), - registrar.view()); + registrar.view(), + new FlutterAssetManager.RegistrarFlutterAssetManager( + registrar.context().getAssets(), registrar)); new FlutterCookieManager(registrar.messenger()); } @@ -69,7 +72,8 @@ private void setUp( BinaryMessenger binaryMessenger, PlatformViewRegistry viewRegistry, Context context, - View containerView) { + View containerView, + FlutterAssetManager flutterAssetManager) { new FlutterCookieManager(binaryMessenger); InstanceManager instanceManager = new InstanceManager(); @@ -111,6 +115,8 @@ private void setUp( binaryMessenger, new WebSettingsHostApiImpl( instanceManager, new WebSettingsHostApiImpl.WebSettingsCreator())); + FlutterAssetManagerHostApi.setup( + binaryMessenger, new FlutterAssetManagerHostApiImpl(flutterAssetManager)); } @Override @@ -120,7 +126,9 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { binding.getBinaryMessenger(), binding.getPlatformViewRegistry(), binding.getApplicationContext(), - null); + null, + new FlutterAssetManager.PluginBindingFlutterAssetManager( + binding.getApplicationContext().getAssets(), binding.getFlutterAssets())); } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java new file mode 100644 index 000000000000..f530365a9334 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java @@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter 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 io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class FlutterAssetManagerHostApiImplTest { + @Mock FlutterAssetManager mockFlutterAssetManager; + + FlutterAssetManagerHostApiImpl testFlutterAssetManagerHostApiImpl; + + @Before + public void setUp() { + mockFlutterAssetManager = mock(FlutterAssetManager.class); + + testFlutterAssetManagerHostApiImpl = + new FlutterAssetManagerHostApiImpl(mockFlutterAssetManager); + } + + @Test + public void list() { + try { + when(mockFlutterAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + List actualFilePaths = testFlutterAssetManagerHostApiImpl.list("test/path"); + verify(mockFlutterAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths.toArray()); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void list_returns_empty_list_when_no_results() { + try { + when(mockFlutterAssetManager.list("test/path")).thenReturn(null); + List actualFilePaths = testFlutterAssetManagerHostApiImpl.list("test/path"); + verify(mockFlutterAssetManager).list("test/path"); + assertArrayEquals(new String[] {}, actualFilePaths.toArray()); + } catch (IOException ex) { + fail(); + } + } + + @Test(expected = RuntimeException.class) + public void list_should_convert_io_exception_to_runtime_exception() { + try { + when(mockFlutterAssetManager.list("test/path")).thenThrow(new IOException()); + testFlutterAssetManagerHostApiImpl.list("test/path"); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void getAssetFilePathByName() { + when(mockFlutterAssetManager.getAssetFilePathByName("index.html")) + .thenReturn("flutter_assets/index.html"); + String filePath = testFlutterAssetManagerHostApiImpl.getAssetFilePathByName("index.html"); + verify(mockFlutterAssetManager).getAssetFilePathByName("index.html"); + assertEquals("flutter_assets/index.html", filePath); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java new file mode 100644 index 000000000000..1f556b7bd486 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java @@ -0,0 +1,54 @@ +// Copyright 2013 The Flutter 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 io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.AssetManager; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterAssets; +import io.flutter.plugins.webviewflutter.FlutterAssetManager.PluginBindingFlutterAssetManager; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class PluginBindingFlutterAssetManagerTest { + @Mock AssetManager mockAssetManager; + @Mock FlutterAssets mockFlutterAssets; + + PluginBindingFlutterAssetManager tesPluginBindingFlutterAssetManager; + + @Before + public void setUp() { + mockAssetManager = mock(AssetManager.class); + mockFlutterAssets = mock(FlutterAssets.class); + + tesPluginBindingFlutterAssetManager = + new PluginBindingFlutterAssetManager(mockAssetManager, mockFlutterAssets); + } + + @Test + public void list() { + try { + when(mockAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + String[] actualFilePaths = tesPluginBindingFlutterAssetManager.list("test/path"); + verify(mockAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void registrar_getAssetFilePathByName() { + tesPluginBindingFlutterAssetManager.getAssetFilePathByName("sample_movie.mp4"); + verify(mockFlutterAssets).getAssetFilePathByName("sample_movie.mp4"); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java new file mode 100644 index 000000000000..86b0fb5432b9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java @@ -0,0 +1,55 @@ +// Copyright 2013 The Flutter 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 io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.AssetManager; +import io.flutter.plugin.common.PluginRegistry.Registrar; +import io.flutter.plugins.webviewflutter.FlutterAssetManager.RegistrarFlutterAssetManager; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +@SuppressWarnings("deprecation") +public class RegistrarFlutterAssetManagerTest { + @Mock AssetManager mockAssetManager; + @Mock Registrar mockRegistrar; + + RegistrarFlutterAssetManager testRegistrarFlutterAssetManager; + + @Before + public void setUp() { + mockAssetManager = mock(AssetManager.class); + mockRegistrar = mock(Registrar.class); + + testRegistrarFlutterAssetManager = + new RegistrarFlutterAssetManager(mockAssetManager, mockRegistrar); + } + + @Test + public void list() { + try { + when(mockAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + String[] actualFilePaths = testRegistrarFlutterAssetManager.list("test/path"); + verify(mockAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void registrar_getAssetFilePathByName() { + testRegistrarFlutterAssetManager.getAssetFilePathByName("sample_movie.mp4"); + verify(mockRegistrar).lookupKeyForAsset("sample_movie.mp4"); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/www/index.html b/packages/webview_flutter/webview_flutter_android/example/assets/www/index.html new file mode 100644 index 000000000000..9895dd3ce6cb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/assets/www/index.html @@ -0,0 +1,20 @@ + + + + +Load file or HTML string example + + + + +

Local demo page

+

+ This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter + webview plugin. +

+ + + \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/www/styles/style.css b/packages/webview_flutter/webview_flutter_android/example/assets/www/styles/style.css new file mode 100644 index 000000000000..c2140b8b0fd8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/assets/www/styles/style.css @@ -0,0 +1,3 @@ +h1 { + color: blue; +} \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index 0c04c8ca4004..3bd283c3e712 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -185,6 +185,7 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, + loadFlutterAsset, loadLocalFile, loadHtmlString, transparentBackground, @@ -226,6 +227,9 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case _MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(controller.data!, context); + break; case _MenuOptions.loadLocalFile: _onLoadLocalFileExample(controller.data!, context); break; @@ -267,6 +271,10 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), const PopupMenuItem<_MenuOptions>( value: _MenuOptions.loadHtmlString, child: Text('Load HTML string'), @@ -357,6 +365,11 @@ class _SampleMenu extends StatelessWidget { await controller.loadUrl('data:text/html;base64,$contentBase64'); } + Future _onLoadFlutterAssetExample( + WebViewController controller, BuildContext context) async { + await controller.loadFlutterAsset('assets/www/index.html'); + } + Future _onLoadLocalFileExample( WebViewController controller, BuildContext context) async { final String pathToIndex = await _prepareLocalFile(); diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart index 395966bdd744..b32deab05477 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -382,6 +382,14 @@ class WebViewController { return _webViewPlatformController.loadFile(absoluteFilePath); } + /// Loads the Flutter asset specified in the pubspec.yaml file. + /// + /// Throws an ArgumentError if [key] is not part of the specified assets + /// in the pubspec.yaml file. + Future loadFlutterAsset(String key) { + return _webViewPlatformController.loadFlutterAsset(key); + } + /// Loads the supplied HTML string. /// /// The [baseUrl] parameter is used when resolving relative URLs within the diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index 59579df8ca50..85990bd02139 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -34,3 +34,5 @@ flutter: assets: - assets/sample_audio.ogg - assets/sample_video.mp4 + - assets/www/index.html + - assets/www/styles/style.css diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart index ecd6f33d1e63..a7561cea687c 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -7,6 +7,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart' show AndroidViewSurface; +import 'android_webview.pigeon.dart'; import 'android_webview_api_impls.dart'; // TODO(bparrishMines): This can be removed once pigeon supports null values: https://github.com/flutter/flutter/issues/59118 @@ -770,3 +771,23 @@ class WebResourceError { /// Describes the error. final String description; } + +/// Manages Flutter assets that are part of Android's app bundle. +class FlutterAssetManager { + /// Constructs the [FlutterAssetManager]. + const FlutterAssetManager(); + + /// Pigeon Host Api implementation for [FlutterAssetManager]. + @visibleForTesting + static FlutterAssetManagerHostApi api = FlutterAssetManagerHostApi(); + + /// Lists all assets at the given path. + /// + /// The assets are returned as a `List`. The `List` only + /// contains files which are direct childs + Future> list(String path) => api.list(path); + + /// Gets the relative file path to the Flutter asset with the given name. + Future getAssetFilePathByName(String name) => + api.getAssetFilePathByName(name); +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart index ae528a64bb8f..f93685611ba3 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart @@ -1616,6 +1616,73 @@ class WebChromeClientHostApi { } } +class _FlutterAssetManagerHostApiCodec extends StandardMessageCodec { + const _FlutterAssetManagerHostApiCodec(); +} + +class FlutterAssetManagerHostApi { + /// Constructor for [FlutterAssetManagerHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + FlutterAssetManagerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _FlutterAssetManagerHostApiCodec(); + + Future> list(String arg_path) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_path]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as List?)!.cast(); + } + } + + Future getAssetFilePathByName(String arg_name) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_name]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as String?)!; + } + } +} + class _WebChromeClientFlutterApiCodec extends StandardMessageCodec { const _WebChromeClientFlutterApiCodec(); } diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart index 0bfa04fde095..25f98742c119 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; - import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'src/android_webview.dart' as android_webview; @@ -20,6 +19,8 @@ class WebViewAndroidWidget extends StatefulWidget { required this.javascriptChannelRegistry, required this.onBuildWidget, @visibleForTesting this.webViewProxy = const WebViewProxy(), + @visibleForTesting + this.flutterAssetManager = const android_webview.FlutterAssetManager(), }); /// Initial parameters used to setup the WebView. @@ -47,6 +48,11 @@ class WebViewAndroidWidget extends StatefulWidget { /// This should only be changed for testing purposes. final WebViewProxy webViewProxy; + /// Manages access to Flutter assets that are part of the Android App bundle. + /// + /// This should only be changed for testing purposes. + final android_webview.FlutterAssetManager flutterAssetManager; + /// Callback to build a widget once [android_webview.WebView] has been initialized. final Widget Function(WebViewAndroidPlatformController controller) onBuildWidget; @@ -67,6 +73,7 @@ class _WebViewAndroidWidgetState extends State { callbacksHandler: widget.callbacksHandler, javascriptChannelRegistry: widget.javascriptChannelRegistry, webViewProxy: widget.webViewProxy, + flutterAssetManager: widget.flutterAssetManager, ); } @@ -91,6 +98,8 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { required this.callbacksHandler, required this.javascriptChannelRegistry, @visibleForTesting this.webViewProxy = const WebViewProxy(), + @visibleForTesting + this.flutterAssetManager = const android_webview.FlutterAssetManager(), }) : assert(creationParams.webSettings?.hasNavigationDelegate != null), super(callbacksHandler) { webView = webViewProxy.createWebView( @@ -134,6 +143,11 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { /// This should only be changed for testing purposes. final WebViewProxy webViewProxy; + /// Manages access to Flutter assets that are part of the Android App bundle. + /// + /// This should only be changed for testing purposes. + final android_webview.FlutterAssetManager flutterAssetManager; + /// Receives callbacks when content should be downloaded instead. @visibleForTesting late final WebViewAndroidDownloadListener downloadListener = @@ -166,6 +180,28 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { return webView.loadUrl(url, {}); } + @override + Future loadFlutterAsset(String key) async { + final String assetFilePath = + await flutterAssetManager.getAssetFilePathByName(key); + final List pathElements = assetFilePath.split('/'); + final String fileName = pathElements.removeLast(); + final List paths = + await flutterAssetManager.list(pathElements.join('/')); + + if (!paths.contains(fileName)) { + throw ArgumentError( + 'Asset for key "$key" not found.', + 'key', + ); + } + + return webView.loadUrl( + 'file:///android_asset/$assetFilePath', + {}, + ); + } + @override Future loadUrl( String url, diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart index 0fdec2c17756..78672ea5b671 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -193,6 +193,13 @@ abstract class WebChromeClientHostApi { void create(int instanceId, int webViewClientInstanceId); } +@HostApi(dartHostTestHandler: 'TestAssetManagerHostApi') +abstract class FlutterAssetManagerHostApi { + List list(String path); + + String getAssetFilePathByName(String name); +} + @FlutterApi() abstract class WebChromeClientFlutterApi { void dispose(int instanceId); diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 75245a31e4c2..bbe9ee174162 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.5.0 +version: 2.6.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.7.0 + webview_flutter_platform_interface: ^1.8.0 dev_dependencies: build_runner: ^2.1.4 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart index 942e59a0b2b3..90c1474f6c67 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart @@ -1055,3 +1055,56 @@ abstract class TestWebChromeClientHostApi { } } } + +class _TestAssetManagerHostApiCodec extends StandardMessageCodec { + const _TestAssetManagerHostApiCodec(); +} + +abstract class TestAssetManagerHostApi { + static const MessageCodec codec = _TestAssetManagerHostApiCodec(); + + List list(String path); + String getAssetFilePathByName(String name); + static void setup(TestAssetManagerHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.list was null.'); + final List args = (message as List?)!; + final String? arg_path = (args[0] as String?); + assert(arg_path != null, + 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.list was null, expected non-null String.'); + final List output = api.list(arg_path!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName was null.'); + final List args = (message as List?)!; + final String? arg_name = (args[0] as String?); + assert(arg_name != null, + 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName was null, expected non-null String.'); + final String output = api.getAssetFilePathByName(arg_name!); + return {'result': output}; + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart index 0e7d5fcd552e..b903678ecef8 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -22,6 +22,7 @@ import 'android_webview_test.mocks.dart'; TestWebSettingsHostApi, TestWebViewClientHostApi, TestWebViewHostApi, + TestAssetManagerHostApi, WebChromeClient, WebView, WebViewClient, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart index 3c1cd610c136..a08019e112ef 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -340,6 +340,27 @@ class MockTestWebViewHostApi extends _i1.Mock String toString() => super.toString(); } +/// A class which mocks [TestAssetManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestAssetManagerHostApi extends _i1.Mock + implements _i3.TestAssetManagerHostApi { + MockTestAssetManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + List list(String? path) => + (super.noSuchMethod(Invocation.method(#list, [path]), + returnValue: []) as List); + @override + String getAssetFilePathByName(String? name) => + (super.noSuchMethod(Invocation.method(#getAssetFilePathByName, [name]), + returnValue: '') as String); + @override + String toString() => super.toString(); +} + /// A class which mocks [WebChromeClient]. /// /// See the documentation for Mockito's code generation for more information. diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart index 460cb54bd393..f3867b313d7d 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart @@ -16,6 +16,7 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte import 'webview_android_widget_test.mocks.dart'; @GenerateMocks([ + android_webview.FlutterAssetManager, android_webview.WebSettings, android_webview.WebView, WebViewAndroidDownloadListener, @@ -30,6 +31,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('$WebViewAndroidWidget', () { + late MockFlutterAssetManager mockFlutterAssetManager; late MockWebView mockWebView; late MockWebSettings mockWebSettings; late MockWebViewProxy mockWebViewProxy; @@ -44,6 +46,7 @@ void main() { late WebViewAndroidPlatformController testController; setUp(() { + mockFlutterAssetManager = MockFlutterAssetManager(); mockWebView = MockWebView(); mockWebSettings = MockWebSettings(); when(mockWebView.settings).thenReturn(mockWebSettings); @@ -77,6 +80,7 @@ void main() { callbacksHandler: mockCallbacksHandler, javascriptChannelRegistry: mockJavascriptChannelRegistry, webViewProxy: mockWebViewProxy, + flutterAssetManager: mockFlutterAssetManager, onBuildWidget: (WebViewAndroidPlatformController controller) { testController = controller; return Container(); @@ -299,6 +303,67 @@ void main() { )); }); + testWidgets('loadFlutterAsset', (WidgetTester tester) async { + await buildWidget(tester); + const String assetKey = 'test_assets/index.html'; + + when(mockFlutterAssetManager.getAssetFilePathByName(assetKey)) + .thenAnswer( + (_) => Future.value('flutter_assets/$assetKey')); + when(mockFlutterAssetManager.list('flutter_assets/test_assets')) + .thenAnswer( + (_) => Future>.value(['index.html'])); + + await testController.loadFlutterAsset(assetKey); + + verify(mockWebView.loadUrl( + 'file:///android_asset/flutter_assets/$assetKey', + {}, + )); + }); + + testWidgets('loadFlutterAsset with file in root', + (WidgetTester tester) async { + await buildWidget(tester); + const String assetKey = 'index.html'; + + when(mockFlutterAssetManager.getAssetFilePathByName(assetKey)) + .thenAnswer( + (_) => Future.value('flutter_assets/$assetKey')); + when(mockFlutterAssetManager.list('flutter_assets')).thenAnswer( + (_) => Future>.value(['index.html'])); + + await testController.loadFlutterAsset(assetKey); + + verify(mockWebView.loadUrl( + 'file:///android_asset/flutter_assets/$assetKey', + {}, + )); + }); + + testWidgets( + 'loadFlutterAsset throws ArgumentError when asset does not exists', + (WidgetTester tester) async { + await buildWidget(tester); + const String assetKey = 'test_assets/index.html'; + + when(mockFlutterAssetManager.getAssetFilePathByName(assetKey)) + .thenAnswer( + (_) => Future.value('flutter_assets/$assetKey')); + when(mockFlutterAssetManager.list('flutter_assets/test_assets')) + .thenAnswer((_) => Future>.value([''])); + + expect( + () => testController.loadFlutterAsset(assetKey), + throwsA( + isA() + .having((ArgumentError error) => error.name, 'name', 'key') + .having((ArgumentError error) => error.message, 'message', + 'Asset for key "$assetKey" not found.'), + ), + ); + }); + testWidgets('loadHtmlString without base URL', (WidgetTester tester) async { await buildWidget(tester); diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart index 6ee53f9f9362..f3b06ea0a0bb 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart @@ -27,6 +27,28 @@ class _FakeJavascriptChannelRegistry_1 extends _i1.Fake class _FakeWebView_2 extends _i1.Fake implements _i2.WebView {} +/// A class which mocks [FlutterAssetManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFlutterAssetManager extends _i1.Mock + implements _i2.FlutterAssetManager { + MockFlutterAssetManager() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future> list(String? path) => + (super.noSuchMethod(Invocation.method(#list, [path]), + returnValue: Future>.value([])) + as _i4.Future>); + @override + _i4.Future getAssetFilePathByName(String? name) => + (super.noSuchMethod(Invocation.method(#getAssetFilePathByName, [name]), + returnValue: Future.value('')) as _i4.Future); + @override + String toString() => super.toString(); +} + /// A class which mocks [WebSettings]. /// /// See the documentation for Mockito's code generation for more information.