[webview_flutter] Android implementation of `loadFlutterAsset` method. (#4581)

diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md
index caee5f7..e8d9e63 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 e70d4e6..37954b3 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 @@
             }
         }
     }
-    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 0000000..1d484d8
--- /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".
+   *
+   * <p>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 0000000..791912a
--- /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}.
+ *
+ * <p>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<String> 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 a5632d3..0123790 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 @@
     }
   }
 
+  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<String> list(String path);
+
+    String getAssetFilePathByName(String name);
+
+    /** The codec used by FlutterAssetManagerHostApi. */
+    static MessageCodec<Object> getCodec() {
+      return FlutterAssetManagerHostApiCodec.INSTANCE;
+    }
+
+    /**
+     * Sets up an instance of `FlutterAssetManagerHostApi` to handle messages through the
+     * `binaryMessenger`.
+     */
+    static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi api) {
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.FlutterAssetManagerHostApi.list", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                Map<String, Object> wrapped = new HashMap<>();
+                try {
+                  ArrayList<Object> args = (ArrayList<Object>) message;
+                  String pathArg = (String) args.get(0);
+                  if (pathArg == null) {
+                    throw new NullPointerException("pathArg unexpectedly null.");
+                  }
+                  List<String> 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<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger,
+                "dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName",
+                getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                Map<String, Object> wrapped = new HashMap<>();
+                try {
+                  ArrayList<Object> args = (ArrayList<Object>) 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 2b174ff..cbeda8d 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 @@
             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 @@
       BinaryMessenger binaryMessenger,
       PlatformViewRegistry viewRegistry,
       Context context,
-      View containerView) {
+      View containerView,
+      FlutterAssetManager flutterAssetManager) {
     new FlutterCookieManager(binaryMessenger);
 
     InstanceManager instanceManager = new InstanceManager();
@@ -111,6 +115,8 @@
         binaryMessenger,
         new WebSettingsHostApiImpl(
             instanceManager, new WebSettingsHostApiImpl.WebSettingsCreator()));
+    FlutterAssetManagerHostApi.setup(
+        binaryMessenger, new FlutterAssetManagerHostApiImpl(flutterAssetManager));
   }
 
   @Override
@@ -120,7 +126,9 @@
         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 0000000..f530365
--- /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<String> 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<String> 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 0000000..1f556b7
--- /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 0000000..86b0fb5
--- /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 0000000..9895dd3
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/assets/www/index.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<!-- 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. -->
+<html lang="en">
+<head>
+<title>Load file or HTML string example</title>
+<link rel="stylesheet" href="styles/style.css" />
+</head>
+<body>
+
+<h1>Local demo page</h1>
+<p>
+  This is an example page used to demonstrate how to load a local file or HTML 
+  string using the <a href="https://pub.dev/packages/webview_flutter">Flutter 
+  webview</a> plugin.
+</p>
+
+</body>
+</html>
\ 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 0000000..c2140b8
--- /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 0c04c8c..3bd283c 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 @@
   listCache,
   clearCache,
   navigationDelegate,
+  loadFlutterAsset,
   loadLocalFile,
   loadHtmlString,
   transparentBackground,
@@ -226,6 +227,9 @@
               case _MenuOptions.navigationDelegate:
                 _onNavigationDelegateExample(controller.data!, context);
                 break;
+              case _MenuOptions.loadFlutterAsset:
+                _onLoadFlutterAssetExample(controller.data!, context);
+                break;
               case _MenuOptions.loadLocalFile:
                 _onLoadLocalFileExample(controller.data!, context);
                 break;
@@ -268,6 +272,10 @@
               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 @@
     await controller.loadUrl('data:text/html;base64,$contentBase64');
   }
 
+  Future<void> _onLoadFlutterAssetExample(
+      WebViewController controller, BuildContext context) async {
+    await controller.loadFlutterAsset('assets/www/index.html');
+  }
+
   Future<void> _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 395966b..b32deab 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 @@
     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<void> 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 59579df..85990bd 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 @@
   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 ecd6f33..a7561ce 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 '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 @@
   /// 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<String>`. The `List<String>` only
+  /// contains files which are direct childs
+  Future<List<String?>> list(String path) => api.list(path);
+
+  /// Gets the relative file path to the Flutter asset with the given name.
+  Future<String> 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 ae528a6..f936856 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 _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<Object?> codec = _FlutterAssetManagerHostApiCodec();
+
+  Future<List<String?>> list(String arg_path) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object>[arg_path]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+        details: null,
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return (replyMap['result'] as List<Object?>?)!.cast<String?>();
+    }
+  }
+
+  Future<String> getAssetFilePathByName(String arg_name) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName',
+        codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object>[arg_name]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+        details: null,
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      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 0bfa04f..25f9874 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 @@
     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 @@
   /// 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 @@
       callbacksHandler: widget.callbacksHandler,
       javascriptChannelRegistry: widget.javascriptChannelRegistry,
       webViewProxy: widget.webViewProxy,
+      flutterAssetManager: widget.flutterAssetManager,
     );
   }
 
@@ -91,6 +98,8 @@
     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 @@
   /// 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 =
@@ -167,6 +181,28 @@
   }
 
   @override
+  Future<void> loadFlutterAsset(String key) async {
+    final String assetFilePath =
+        await flutterAssetManager.getAssetFilePathByName(key);
+    final List<String> pathElements = assetFilePath.split('/');
+    final String fileName = pathElements.removeLast();
+    final List<String?> 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',
+      <String, String>{},
+    );
+  }
+
+  @override
   Future<void> loadUrl(
     String url,
     Map<String, String>? headers,
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 0fdec2c..78672ea 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 @@
   void create(int instanceId, int webViewClientInstanceId);
 }
 
+@HostApi(dartHostTestHandler: 'TestAssetManagerHostApi')
+abstract class FlutterAssetManagerHostApi {
+  List<String> 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 75245a3..bbe9ee1 100644
--- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml
@@ -2,7 +2,7 @@
 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 @@
 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 942e59a..90c1474 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 @@
     }
   }
 }
+
+class _TestAssetManagerHostApiCodec extends StandardMessageCodec {
+  const _TestAssetManagerHostApiCodec();
+}
+
+abstract class TestAssetManagerHostApi {
+  static const MessageCodec<Object?> codec = _TestAssetManagerHostApiCodec();
+
+  List<String?> list(String path);
+  String getAssetFilePathByName(String name);
+  static void setup(TestAssetManagerHostApi? api,
+      {BinaryMessenger? binaryMessenger}) {
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          '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<Object?> args = (message as List<Object?>?)!;
+          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<String?> output = api.list(arg_path!);
+          return <Object?, Object?>{'result': output};
+        });
+      }
+    }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          '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<Object?> args = (message as List<Object?>?)!;
+          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 <Object?, Object?>{'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 0e7d5fc..b903678 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 @@
   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 3c1cd61..a08019e 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 @@
   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<String?> list(String? path) =>
+      (super.noSuchMethod(Invocation.method(#list, [path]),
+          returnValue: <String?>[]) as List<String?>);
+  @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 460cb54..f3867b3 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 'webview_android_widget_test.mocks.dart';
 
 @GenerateMocks(<Type>[
+  android_webview.FlutterAssetManager,
   android_webview.WebSettings,
   android_webview.WebView,
   WebViewAndroidDownloadListener,
@@ -30,6 +31,7 @@
   TestWidgetsFlutterBinding.ensureInitialized();
 
   group('$WebViewAndroidWidget', () {
+    late MockFlutterAssetManager mockFlutterAssetManager;
     late MockWebView mockWebView;
     late MockWebSettings mockWebSettings;
     late MockWebViewProxy mockWebViewProxy;
@@ -44,6 +46,7 @@
     late WebViewAndroidPlatformController testController;
 
     setUp(() {
+      mockFlutterAssetManager = MockFlutterAssetManager();
       mockWebView = MockWebView();
       mockWebSettings = MockWebSettings();
       when(mockWebView.settings).thenReturn(mockWebSettings);
@@ -77,6 +80,7 @@
         callbacksHandler: mockCallbacksHandler,
         javascriptChannelRegistry: mockJavascriptChannelRegistry,
         webViewProxy: mockWebViewProxy,
+        flutterAssetManager: mockFlutterAssetManager,
         onBuildWidget: (WebViewAndroidPlatformController controller) {
           testController = controller;
           return Container();
@@ -299,6 +303,67 @@
         ));
       });
 
+      testWidgets('loadFlutterAsset', (WidgetTester tester) async {
+        await buildWidget(tester);
+        const String assetKey = 'test_assets/index.html';
+
+        when(mockFlutterAssetManager.getAssetFilePathByName(assetKey))
+            .thenAnswer(
+                (_) => Future<String>.value('flutter_assets/$assetKey'));
+        when(mockFlutterAssetManager.list('flutter_assets/test_assets'))
+            .thenAnswer(
+                (_) => Future<List<String>>.value(<String>['index.html']));
+
+        await testController.loadFlutterAsset(assetKey);
+
+        verify(mockWebView.loadUrl(
+          'file:///android_asset/flutter_assets/$assetKey',
+          <String, String>{},
+        ));
+      });
+
+      testWidgets('loadFlutterAsset with file in root',
+          (WidgetTester tester) async {
+        await buildWidget(tester);
+        const String assetKey = 'index.html';
+
+        when(mockFlutterAssetManager.getAssetFilePathByName(assetKey))
+            .thenAnswer(
+                (_) => Future<String>.value('flutter_assets/$assetKey'));
+        when(mockFlutterAssetManager.list('flutter_assets')).thenAnswer(
+            (_) => Future<List<String>>.value(<String>['index.html']));
+
+        await testController.loadFlutterAsset(assetKey);
+
+        verify(mockWebView.loadUrl(
+          'file:///android_asset/flutter_assets/$assetKey',
+          <String, String>{},
+        ));
+      });
+
+      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<String>.value('flutter_assets/$assetKey'));
+        when(mockFlutterAssetManager.list('flutter_assets/test_assets'))
+            .thenAnswer((_) => Future<List<String>>.value(<String>['']));
+
+        expect(
+          () => testController.loadFlutterAsset(assetKey),
+          throwsA(
+            isA<ArgumentError>()
+                .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 6ee53f9..f3b06ea 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 _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?>> list(String? path) =>
+      (super.noSuchMethod(Invocation.method(#list, [path]),
+              returnValue: Future<List<String?>>.value(<String?>[]))
+          as _i4.Future<List<String?>>);
+  @override
+  _i4.Future<String> getAssetFilePathByName(String? name) =>
+      (super.noSuchMethod(Invocation.method(#getAssetFilePathByName, [name]),
+          returnValue: Future<String>.value('')) as _i4.Future<String>);
+  @override
+  String toString() => super.toString();
+}
+
 /// A class which mocks [WebSettings].
 ///
 /// See the documentation for Mockito's code generation for more information.