[pigeon] made it so async java handlers can report errors (#460)

diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index f3cd21c..b91c59d 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.0.2
+
+* [java] Made it so `@async` handlers in `@HostApi()` can report errors
+  explicitly.
+
 ## 1.0.1
 * [front-end] Fixed bug where classes only referenced as type arguments for
   generics weren't being generated.
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 862170e..fea3f11 100644
--- a/packages/pigeon/lib/generator_tools.dart
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -8,7 +8,7 @@
 import 'ast.dart';
 
 /// The current version of pigeon. This must match the version in pubspec.yaml.
-const String pigeonVersion = '1.0.1';
+const String pigeonVersion = '1.0.2';
 
 /// Read all the content from [stdin] to a String.
 String readStdin() {
diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart
index 3553703..2a4aac0 100644
--- a/packages/pigeon/lib/java_generator.dart
+++ b/packages/pigeon/lib/java_generator.dart
@@ -160,7 +160,9 @@
           indent.scoped('{', '} else {', () {
             indent.write('channel.setMessageHandler((message, reply) -> ');
             indent.scoped('{', '});', () {
-              final String returnType = _javaTypeForDartType(method.returnType);
+              final String returnType = method.returnType.isVoid
+                  ? 'Void'
+                  : _javaTypeForDartType(method.returnType);
               indent.writeln('Map<String, Object> wrapped = new HashMap<>();');
               indent.write('try ');
               indent.scoped('{', '}', () {
@@ -184,12 +186,20 @@
                 if (method.isAsynchronous) {
                   final String resultValue =
                       method.returnType.isVoid ? 'null' : 'result';
-                  methodArgument.add(
-                    'result -> { '
-                    'wrapped.put("${Keys.result}", $resultValue); '
-                    'reply.reply(wrapped); '
-                    '}',
-                  );
+                  const String resultName = 'resultCallback';
+                  indent.format('''
+Result<$returnType> $resultName = new Result<$returnType>() {
+\tpublic void success($returnType result) {
+\t\twrapped.put("${Keys.result}", $resultValue);
+\t\treply.reply(wrapped);
+\t}
+\tpublic void error(Throwable error) {
+\t\twrapped.put("${Keys.error}", wrapError(error));
+\t\treply.reply(wrapped);
+\t}
+};
+''');
+                  methodArgument.add(resultName);
                 }
                 final String call =
                     'api.${method.name}(${methodArgument.join(', ')})';
@@ -493,6 +503,7 @@
       indent.write('public interface Result<T> ');
       indent.scoped('{', '}', () {
         indent.writeln('void success(T result);');
+        indent.writeln('void error(Throwable error);');
       });
     }
 
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/AsyncTest.java b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/AsyncTest.java
new file mode 100644
index 0000000..c8da07d
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/AsyncTest.java
@@ -0,0 +1,98 @@
+// 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 com.example.android_unit_tests;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import com.example.android_unit_tests.AsyncHandlers.*;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MessageCodec;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class AsyncTest {
+  class Success implements Api2Host {
+    @Override
+    public void calculate(Value value, Result<Value> result) {
+      result.success(value);
+    }
+
+    @Override
+    public void voidVoid(Result<Void> result) {
+      result.success(null);
+    }
+  }
+
+  class Error implements Api2Host {
+    @Override
+    public void calculate(Value value, Result<Value> result) {
+      result.error(new Exception("error"));
+    }
+
+    @Override
+    public void voidVoid(Result<Void> result) {
+      result.error(new Exception("error"));
+    }
+  }
+
+  @Test
+  public void asyncSuccess() {
+    Success api = new Success();
+    BinaryMessenger binaryMessenger = mock(BinaryMessenger.class);
+    Api2Host.setup(binaryMessenger, api);
+    ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> handler =
+        ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
+    verify(binaryMessenger).setMessageHandler(eq("dev.flutter.pigeon.Api2Host.calculate"), any());
+    verify(binaryMessenger)
+        .setMessageHandler(eq("dev.flutter.pigeon.Api2Host.voidVoid"), handler.capture());
+    MessageCodec<Object> codec = Pigeon.Api.getCodec();
+    ByteBuffer message = codec.encodeMessage(null);
+    Boolean[] didCall = {false};
+    handler
+        .getValue()
+        .onMessage(
+            message,
+            (bytes) -> {
+              bytes.rewind();
+              @SuppressWarnings("unchecked")
+              Map<String, Object> wrapped = (Map<String, Object>) codec.decodeMessage(bytes);
+              assertTrue(wrapped.containsKey("result"));
+              didCall[0] = true;
+            });
+    assertTrue(didCall[0]);
+  }
+
+  @Test
+  public void asyncError() {
+    Error api = new Error();
+    BinaryMessenger binaryMessenger = mock(BinaryMessenger.class);
+    Api2Host.setup(binaryMessenger, api);
+    ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> handler =
+        ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
+    verify(binaryMessenger).setMessageHandler(eq("dev.flutter.pigeon.Api2Host.calculate"), any());
+    verify(binaryMessenger)
+        .setMessageHandler(eq("dev.flutter.pigeon.Api2Host.voidVoid"), handler.capture());
+    MessageCodec<Object> codec = Pigeon.Api.getCodec();
+    ByteBuffer message = codec.encodeMessage(null);
+    Boolean[] didCall = {false};
+    handler
+        .getValue()
+        .onMessage(
+            message,
+            (bytes) -> {
+              bytes.rewind();
+              @SuppressWarnings("unchecked")
+              Map<String, Object> wrapped = (Map<String, Object>) codec.decodeMessage(bytes);
+              assertTrue(wrapped.containsKey("error"));
+              assertEquals(
+                  "java.lang.Exception: error", ((Map) wrapped.get("error")).get("message"));
+              didCall[0] = true;
+            });
+    assertTrue(didCall[0]);
+  }
+}
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index 1b49a05..99fa15f 100644
--- a/packages/pigeon/pubspec.yaml
+++ b/packages/pigeon/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
 repository: https://github.com/flutter/packages/tree/master/packages/pigeon
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon
-version: 1.0.1 # This must match the version in lib/generator_tools.dart
+version: 1.0.2 # This must match the version in lib/generator_tools.dart
 
 environment:
   sdk: '>=2.12.0 <3.0.0'
diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart
index 3ed6590..5fe056a 100644
--- a/packages/pigeon/test/java_generator_test.dart
+++ b/packages/pigeon/test/java_generator_test.dart
@@ -514,12 +514,10 @@
     final String code = sink.toString();
     expect(code, contains('public interface Api'));
     expect(code, contains('public interface Result<T> {'));
+    expect(code, contains('void error(Throwable error);'));
     expect(
         code, contains('void doSomething(Input arg, Result<Output> result);'));
-    expect(
-        code,
-        contains(
-            'api.doSomething(argArg, result -> { wrapped.put("result", result); reply.reply(wrapped); });'));
+    expect(code, contains('api.doSomething(argArg, resultCallback);'));
     expect(code, contains('channel.setMessageHandler(null)'));
   });