[pigeon] added android unittests (#327)

diff --git a/packages/pigeon/CONTRIBUTING.md b/packages/pigeon/CONTRIBUTING.md
index ca1aaaf..d4174c8 100644
--- a/packages/pigeon/CONTRIBUTING.md
+++ b/packages/pigeon/CONTRIBUTING.md
@@ -72,6 +72,3 @@
   reached the limitations of using dart:mirrors for parsing the Dart files.
   That package has been deprecated and it doesn't support null-safe annotations.
   We should migrate to using the Dart Analyzer as the front-end parser.
-* Integration tests for Android - Right now we have integration tests for Dart
-  and iOS.  We should add some for Android.  It is limiting the speed at which
-  we can bring in external contributor's contributions to Android / Java.
diff --git a/packages/pigeon/pigeons/android_unittests.dart b/packages/pigeon/pigeons/android_unittests.dart
new file mode 100644
index 0000000..baaa69b
--- /dev/null
+++ b/packages/pigeon/pigeons/android_unittests.dart
@@ -0,0 +1,24 @@
+// Copyright 2020 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.
+
+import 'package:pigeon/pigeon.dart';
+
+class SetRequest {
+  int? value;
+}
+
+class NestedRequest {
+  String? context;
+  SetRequest? request;
+}
+
+@HostApi()
+abstract class Api {
+  void setValue(SetRequest request);
+}
+
+@HostApi()
+abstract class NestedApi {
+  void setValueWithContext(NestedRequest request);
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/.gitignore b/packages/pigeon/platform_tests/android_unit_tests/.gitignore
new file mode 100644
index 0000000..0fa6b67
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/.gitignore
@@ -0,0 +1,46 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/packages/pigeon/platform_tests/android_unit_tests/.metadata b/packages/pigeon/platform_tests/android_unit_tests/.metadata
new file mode 100644
index 0000000..62b61c3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 891511d58f6550ce9e9b03b8d7c6a602caa97488
+  channel: master
+
+project_type: app
diff --git a/packages/pigeon/platform_tests/android_unit_tests/README.md b/packages/pigeon/platform_tests/android_unit_tests/README.md
new file mode 100644
index 0000000..e109aa5
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/README.md
@@ -0,0 +1,3 @@
+# android_unit_tests
+
+Unit-tests for Pigeon generated Java code.  See [../../run_tests.sh](../../run_tests.sh).
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/.gitignore b/packages/pigeon/platform_tests/android_unit_tests/android/.gitignore
new file mode 100644
index 0000000..0a741cb
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/.gitignore
@@ -0,0 +1,11 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/build.gradle b/packages/pigeon/platform_tests/android_unit_tests/android/app/build.gradle
new file mode 100644
index 0000000..f45bc0b
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/build.gradle
@@ -0,0 +1,65 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 30
+
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId "com.example.android_unit_tests"
+        minSdkVersion 16
+        targetSdkVersion 30
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+    }
+
+    buildTypes {
+        release {
+            // TODO: Add your own signing config for the release build.
+            // Signing with the debug keys for now, so `flutter run --release` works.
+            signingConfig signingConfigs.debug
+        }
+    }
+
+    testOptions {
+        unitTests.returnDefaultValues = true
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    testImplementation 'junit:junit:4.+'
+    testImplementation "org.mockito:mockito-core:3.+"
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/debug/AndroidManifest.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..0102266
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android_unit_tests">
+    <!-- Flutter needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/AndroidManifest.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a945186
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android_unit_tests">
+   <application
+        android:label="android_unit_tests"
+        android:icon="@mipmap/ic_launcher">
+        <activity
+            android:name=".MainActivity"
+            android:launchMode="singleTop"
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:hardwareAccelerated="true"
+            android:windowSoftInputMode="adjustResize">
+            <!-- Specifies an Android theme to apply to this Activity as soon as
+                 the Android process has started. This theme is visible to the user
+                 while the Flutter UI initializes. After that, this theme continues
+                 to determine the Window background behind the Flutter UI. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.NormalTheme"
+              android:resource="@style/NormalTheme"
+              />
+            <!-- Displays an Android View that continues showing the launch screen
+                 Drawable until Flutter paints its first frame, then this splash
+                 screen fades out. A splash screen is useful to avoid any visual
+                 gap between the end of Android's launch screen and the painting of
+                 Flutter's first frame. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.SplashScreenDrawable"
+              android:resource="@drawable/launch_background"
+              />
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <!-- Don't delete the meta-data below.
+             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+        <meta-data
+            android:name="flutterEmbedding"
+            android:value="2" />
+    </application>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java
new file mode 100644
index 0000000..6219796
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java
@@ -0,0 +1,81 @@
+// Autogenerated from Pigeon (v0.2.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package com.example.android_unit_tests;
+
+import io.flutter.plugin.common.BasicMessageChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Generated class from Pigeon. */
+@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
+public class Pigeon {
+
+  /** Generated class from Pigeon that represents data sent in messages. */
+  public static class SetRequest {
+    private Long value;
+
+    public Long getValue() {
+      return value;
+    }
+
+    public void setValue(Long setterArg) {
+      this.value = setterArg;
+    }
+
+    Map<String, Object> toMap() {
+      Map<String, Object> toMapResult = new HashMap<>();
+      toMapResult.put("value", value);
+      return toMapResult;
+    }
+
+    static SetRequest fromMap(Map<String, Object> map) {
+      SetRequest fromMapResult = new SetRequest();
+      Object value = map.get("value");
+      fromMapResult.value =
+          (value == null) ? null : ((value instanceof Integer) ? (Integer) value : (Long) value);
+      return fromMapResult;
+    }
+  }
+
+  /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+  public interface Api {
+    void setValue(SetRequest arg);
+
+    /** Sets up an instance of `Api` to handle messages through the `binaryMessenger`. */
+    static void setup(BinaryMessenger binaryMessenger, Api api) {
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.Api.setValue", new StandardMessageCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                Map<String, Object> wrapped = new HashMap<>();
+                try {
+                  @SuppressWarnings("ConstantConditions")
+                  SetRequest input = SetRequest.fromMap((Map<String, Object>) message);
+                  api.setValue(input);
+                  wrapped.put("result", null);
+                } catch (Error | RuntimeException exception) {
+                  wrapped.put("error", wrapError(exception));
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+    }
+  }
+
+  private static Map<String, Object> wrapError(Throwable exception) {
+    Map<String, Object> errorMap = new HashMap<>();
+    errorMap.put("message", exception.toString());
+    errorMap.put("code", exception.getClass().getSimpleName());
+    errorMap.put("details", null);
+    return errorMap;
+  }
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/kotlin/com/example/android_unit_tests/MainActivity.kt b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/kotlin/com/example/android_unit_tests/MainActivity.kt
new file mode 100644
index 0000000..eb1afff
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/kotlin/com/example/android_unit_tests/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.android_unit_tests
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="?android:colorBackground" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable/launch_background.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values-night/styles.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..449a9f9
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+         
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values/styles.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d74aa35
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+         
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/profile/AndroidManifest.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..0102266
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android_unit_tests">
+    <!-- Flutter needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PigeonTest.java b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PigeonTest.java
new file mode 100644
index 0000000..1efa547
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PigeonTest.java
@@ -0,0 +1,98 @@
+package com.example.android_unit_tests;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MessageCodec;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class PigeonTest {
+  private MessageCodec<Object> codec = StandardMessageCodec.INSTANCE;
+
+  @Test
+  public void toMapAndBack() {
+    Pigeon.SetRequest request = new Pigeon.SetRequest();
+    request.setValue(1234l);
+    Map<String, Object> map = request.toMap();
+    Pigeon.SetRequest readRequest = Pigeon.SetRequest.fromMap(map);
+    assertEquals(request.getValue(), readRequest.getValue());
+  }
+
+  @Test
+  public void toMapAndBackNested() {
+    Pigeon.NestedRequest nested = new Pigeon.NestedRequest();
+    Pigeon.SetRequest request = new Pigeon.SetRequest();
+    request.setValue(1234l);
+    nested.setRequest(request);
+    Map<String, Object> map = nested.toMap();
+    Pigeon.NestedRequest readNested = Pigeon.NestedRequest.fromMap(map);
+    assertEquals(nested.getRequest().getValue(), readNested.getRequest().getValue());
+  }
+
+  @Test
+  public void clearsHandler() {
+    Pigeon.Api mockApi = mock(Pigeon.Api.class);
+    BinaryMessenger binaryMessenger = mock(BinaryMessenger.class);
+    Pigeon.Api.setup(binaryMessenger, mockApi);
+    ArgumentCaptor<String> channelName = ArgumentCaptor.forClass(String.class);
+    verify(binaryMessenger).setMessageHandler(channelName.capture(), isNotNull());
+    Pigeon.Api.setup(binaryMessenger, null);
+    verify(binaryMessenger).setMessageHandler(eq(channelName.getValue()), isNull());
+  }
+
+  /** Causes an exception in the handler by passing in null when a SetRequest is expected. */
+  @Test
+  public void errorMessage() {
+    Pigeon.Api mockApi = mock(Pigeon.Api.class);
+    BinaryMessenger binaryMessenger = mock(BinaryMessenger.class);
+    Pigeon.Api.setup(binaryMessenger, mockApi);
+    ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> handler =
+        ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
+    verify(binaryMessenger).setMessageHandler(anyString(), handler.capture());
+    ByteBuffer message = codec.encodeMessage(null);
+    handler
+        .getValue()
+        .onMessage(
+            message,
+            (bytes) -> {
+              bytes.rewind();
+              @SuppressWarnings("unchecked")
+              Map<String, Object> wrapped = (Map<String, Object>) codec.decodeMessage(bytes);
+              assertTrue(wrapped.containsKey("error"));
+            });
+  }
+
+  @Test
+  public void callsVoidMethod() {
+    Pigeon.Api mockApi = mock(Pigeon.Api.class);
+    BinaryMessenger binaryMessenger = mock(BinaryMessenger.class);
+    Pigeon.Api.setup(binaryMessenger, mockApi);
+    ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> handler =
+        ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
+    verify(binaryMessenger).setMessageHandler(anyString(), handler.capture());
+    Pigeon.SetRequest request = new Pigeon.SetRequest();
+    request.setValue(1234l);
+    ByteBuffer message = codec.encodeMessage(request.toMap());
+    message.rewind();
+    handler
+        .getValue()
+        .onMessage(
+            message,
+            (bytes) -> {
+              bytes.rewind();
+              @SuppressWarnings("unchecked")
+              Map<String, Object> wrapped = (Map<String, Object>) codec.decodeMessage(bytes);
+              assertTrue(wrapped.containsKey("result"));
+              assertNull(wrapped.get("result"));
+            });
+    ArgumentCaptor<Pigeon.SetRequest> receivedRequest =
+        ArgumentCaptor.forClass(Pigeon.SetRequest.class);
+    verify(mockApi).setValue(receivedRequest.capture());
+    assertEquals(request.getValue(), receivedRequest.getValue().getValue());
+  }
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/build.gradle b/packages/pigeon/platform_tests/android_unit_tests/android/build.gradle
new file mode 100644
index 0000000..8a8ae3d
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/build.gradle
@@ -0,0 +1,34 @@
+buildscript {
+    ext.kotlin_version = '1.3.50'
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:4.1.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+    gradle.projectsEvaluated {
+        tasks.withType(JavaCompile) {
+            options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+        }
+    }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+    project.buildDir = "${rootProject.buildDir}/${project.name}"
+    project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/gradle.properties b/packages/pigeon/platform_tests/android_unit_tests/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/gradle/wrapper/gradle-wrapper.properties b/packages/pigeon/platform_tests/android_unit_tests/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..bc6a58a
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/settings.gradle b/packages/pigeon/platform_tests/android_unit_tests/android/settings.gradle
new file mode 100644
index 0000000..44e62bc
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/settings.gradle
@@ -0,0 +1,11 @@
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/packages/pigeon/platform_tests/android_unit_tests/lib/main.dart b/packages/pigeon/platform_tests/android_unit_tests/lib/main.dart
new file mode 100644
index 0000000..5e49fc1
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/lib/main.dart
@@ -0,0 +1,5 @@
+// Copyright 2020 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.
+
+void main() {}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/pubspec.yaml b/packages/pigeon/platform_tests/android_unit_tests/pubspec.yaml
new file mode 100644
index 0000000..febf7cf
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/pubspec.yaml
@@ -0,0 +1,20 @@
+name: android_unit_tests
+description: Unit tests for Pigeon generated Java code.
+
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+version: 1.0.0+1
+
+environment:
+  sdk: ">=2.7.0 <3.0.0"
+
+dependencies:
+  cupertino_icons: ^1.0.2
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+flutter:
+  uses-material-design: true
diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh
index 82f09bb..07d38bb 100755
--- a/packages/pigeon/run_tests.sh
+++ b/packages/pigeon/run_tests.sh
@@ -329,9 +329,27 @@
   pub global activate flutter_plugin_tools && pub global run flutter_plugin_tools format 2>/dev/null
 }
 
+run_android_unittests() {
+  pushd $PWD
+  pub run pigeon \
+    --input pigeons/android_unittests.dart \
+    --dart_out /dev/null \
+    --java_out platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java \
+    --java_package "com.example.android_unit_tests"
+  
+  cd platform_tests/android_unit_tests
+  if [ ! -f "android/gradlew" ]; then
+    flutter build apk --debug
+  fi
+  cd android
+  ./gradlew test
+  popd
+}
+
 ###############################################################################
 # main
 ###############################################################################
+should_run_android_unittests=true
 should_run_dart_compilation_tests=true
 should_run_dart_unittests=true
 should_run_flutter_unittests=true
@@ -344,6 +362,7 @@
 while getopts "t:l?h" opt; do
   case $opt in
   t)
+    should_run_android_unittests=false
     should_run_dart_compilation_tests=false
     should_run_dart_unittests=false
     should_run_flutter_unittests=false
@@ -354,6 +373,7 @@
     should_run_mock_handler_tests=false
     should_run_objc_compilation_tests=false
     case $OPTARG in
+    android_unittests) should_run_android_unittests=true ;;
     dart_compilation_tests) should_run_dart_compilation_tests=true ;;
     dart_unittests) should_run_dart_unittests=true ;;
     flutter_unittests) should_run_flutter_unittests=true ;;
@@ -370,6 +390,7 @@
     ;;
   l)
     echo "available tests for -t:
+  android_unittests      - Unit tests on generated Java code.
   dart_compilation_tests - Compilation tests on generated Dart code.
   dart_unittests         - Unit tests on and analysis on Pigeon's implementation.
   flutter_unittests      - Unit tests on generated Dart code.
@@ -425,6 +446,9 @@
 if [ "$should_run_ios_e2e_tests" = true ]; then
   run_ios_e2e_tests
 fi
+if [ "$should_run_android_unittests" = true ]; then
+  run_android_unittests
+fi
 if [ "$should_run_formatter" = true ]; then
   run_formatter
 fi