Add tests for StandardMethodCodec (#18521)

diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn
index 05b176b..8869ead 100644
--- a/shell/platform/android/BUILD.gn
+++ b/shell/platform/android/BUILD.gn
@@ -439,6 +439,7 @@
     "test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java",
     "test/io/flutter/external/FlutterLaunchTests.java",
     "test/io/flutter/plugin/common/StandardMessageCodecTest.java",
+    "test/io/flutter/plugin/common/StandardMethodCodecTest.java",
     "test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java",
     "test/io/flutter/plugin/editing/TextInputPluginTest.java",
     "test/io/flutter/plugin/platform/PlatformPluginTest.java",
diff --git a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java
index 65ea239..f8cbb2c 100644
--- a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java
+++ b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java
@@ -166,8 +166,8 @@
      * Handles a successful result.
      *
      * @param result The result, possibly null. The result must be an Object type supported by the
-     *     codec. For instance, if you are using StandardCodec (default), please see {@link
-     *     StandardMessageCodec} documentation on what types are supported.
+     *     codec. For instance, if you are using {@link StandardMessageCodec} (default), please see
+     *     its documentation on what types are supported.
      */
     @UiThread
     void success(@Nullable Object result);
@@ -178,8 +178,8 @@
      * @param errorCode An error code String.
      * @param errorMessage A human-readable error message String, possibly null.
      * @param errorDetails Error details, possibly null. The details must be an Object type
-     *     supported by the codec. For instance, if you are using StandardCodec (default), please
-     *     see {@link StandardMessageCodec} documentation on what types are supported.
+     *     supported by the codec. For instance, if you are using {@link StandardMessageCodec}
+     *     (default), please see its documentation on what types are supported.
      */
     @UiThread
     void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails);
diff --git a/shell/platform/android/io/flutter/plugin/common/MethodCodec.java b/shell/platform/android/io/flutter/plugin/common/MethodCodec.java
index 8c9a867..fba950f 100644
--- a/shell/platform/android/io/flutter/plugin/common/MethodCodec.java
+++ b/shell/platform/android/io/flutter/plugin/common/MethodCodec.java
@@ -48,7 +48,8 @@
    *
    * @param errorCode An error code String.
    * @param errorMessage An error message String, possibly null.
-   * @param errorDetails Error details, possibly null.
+   * @param errorDetails Error details, possibly null. Consider supporting {@link Throwable} in your
+   *     codec. This is the most common value passed to this field.
    * @return a {@link ByteBuffer} containing the encoding between position 0 and the current
    *     position.
    */
diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java
index 509c91c..820e428 100644
--- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java
+++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java
@@ -17,6 +17,7 @@
 import io.flutter.embedding.engine.renderer.FlutterRendererTest;
 import io.flutter.external.FlutterLaunchTests;
 import io.flutter.plugin.common.StandardMessageCodecTest;
+import io.flutter.plugin.common.StandardMethodCodecTest;
 import io.flutter.plugin.editing.InputConnectionAdaptorTest;
 import io.flutter.plugin.editing.TextInputPluginTest;
 import io.flutter.plugin.platform.PlatformPluginTest;
@@ -52,6 +53,7 @@
   PreconditionsTest.class,
   RenderingComponentTest.class,
   StandardMessageCodecTest.class,
+  StandardMethodCodecTest.class,
   ShimPluginRegistryTest.class,
   SingleViewPresentationTest.class,
   SmokeTest.class,
diff --git a/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java b/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java
new file mode 100644
index 0000000..c99a30f
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java
@@ -0,0 +1,92 @@
+package io.flutter.plugin.common;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@Config(manifest = Config.NONE)
+@RunWith(RobolectricTestRunner.class)
+public class StandardMethodCodecTest {
+
+  @Test
+  public void encodeMethodTest() {
+    final Map<String, String> args = new HashMap<>();
+    args.put("testArg", "testValue");
+    MethodCall call = new MethodCall("testMethod", args);
+    final ByteBuffer buffer = StandardMethodCodec.INSTANCE.encodeMethodCall(call);
+    assertNotNull(buffer);
+    buffer.flip();
+    final MethodCall result = StandardMethodCodec.INSTANCE.decodeMethodCall(buffer);
+    assertEquals(call.method, result.method);
+    assertEquals(call.arguments, result.arguments);
+  }
+
+  @Test
+  public void encodeSuccessEnvelopeTest() {
+    final Map<String, Integer> success = new HashMap<>();
+    success.put("result", 1);
+    final ByteBuffer buffer = StandardMethodCodec.INSTANCE.encodeSuccessEnvelope(success);
+    assertNotNull(buffer);
+    buffer.flip();
+    final Object result = StandardMethodCodec.INSTANCE.decodeEnvelope(buffer);
+    assertEquals(success, result);
+  }
+
+  @Test
+  public void encodeSuccessEnvelopeUnsupportedObjectTest() {
+    final StandardMethodCodecTest joke = new StandardMethodCodecTest();
+    try {
+      final ByteBuffer buffer = StandardMethodCodec.INSTANCE.encodeSuccessEnvelope(joke);
+      fail("Should have failed to convert unsupported type.");
+    } catch (IllegalArgumentException e) {
+      // pass.
+    }
+  }
+
+  @Test
+  public void encodeErrorEnvelopeWithNullDetailsTest() {
+    final ByteBuffer buffer =
+        StandardMethodCodec.INSTANCE.encodeErrorEnvelope("code", "error", null);
+    assertNotNull(buffer);
+    buffer.flip();
+    try {
+      StandardMethodCodec.INSTANCE.decodeEnvelope(buffer);
+      fail("Should have thrown a FlutterException since this is an error envelope.");
+    } catch (FlutterException result) {
+      assertEquals("code", result.code);
+      assertEquals("error", result.getMessage());
+      assertNull(result.details);
+    }
+  }
+
+  @Test
+  public void encodeErrorEnvelopeWithThrowableTest() {
+    final Exception e = new IllegalArgumentException("foo");
+    final ByteBuffer buffer =
+        StandardMethodCodec.INSTANCE.encodeErrorEnvelope("code", e.getMessage(), e);
+    assertNotNull(buffer);
+    buffer.flip();
+    try {
+      StandardMethodCodec.INSTANCE.decodeEnvelope(buffer);
+      fail("Should have thrown a FlutterException since this is an error envelope.");
+    } catch (FlutterException result) {
+      assertEquals("code", result.code);
+      assertEquals("foo", result.getMessage());
+      // Must contain part of a stack.
+      String stack = (String) result.details;
+      assertTrue(
+          stack.contains(
+              "at io.flutter.plugin.common.StandardMethodCodecTest.encodeErrorEnvelopeWithThrowableTest(StandardMethodCodecTest.java:"));
+    }
+  }
+}