[pigeon] implements background platform channels support (#1022)

* [pigeon] added support for background platform channels

* added objc support

* updated the docstring

* added java support

* added error if taskqueues are used with flutter apis

* added compilation test for java

* added ios compilation test

* updated readme and version

* stuart feedback
diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index 31676bd..2e8c814 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.1
+
+* Adds support for TaskQueues for serial background execution.
+
 ## 2.0.0
 
 * Implements nullable parameters.
diff --git a/packages/pigeon/README.md b/packages/pigeon/README.md
index e773591..149e2c1 100644
--- a/packages/pigeon/README.md
+++ b/packages/pigeon/README.md
@@ -165,6 +165,22 @@
 }
 ```
 
+### TaskQueues
+
+When targeting a Flutter version that supports the
+[TaskQueue API](https://docs.flutter.dev/development/platform-integration/platform-channels?tab=type-mappings-kotlin-tab#channels-and-platform-threading)
+the threading model for handling HostApi methods can be selected with the
+`TaskQueue` annotation:
+
+```dart
+@HostApi()
+abstract class Api2Host {
+  @TaskQueue(type: TaskQueueType.serialBackgroundThread)
+  int add(int x, int y);
+}
+```
+
+
 ## Feedback
 
 File an issue in [flutter/flutter](https://github.com/flutter/flutter) with the
diff --git a/packages/pigeon/lib/ast.dart b/packages/pigeon/lib/ast.dart
index c9d284c..faffc7b 100644
--- a/packages/pigeon/lib/ast.dart
+++ b/packages/pigeon/lib/ast.dart
@@ -4,6 +4,7 @@
 
 import 'package:collection/collection.dart' show ListEquality;
 import 'package:meta/meta.dart';
+import 'pigeon_lib.dart';
 
 final Function _listEquals = const ListEquality<dynamic>().equals;
 
@@ -29,6 +30,7 @@
     this.isAsynchronous = false,
     this.offset,
     this.objcSelector = '',
+    this.taskQueueType = TaskQueueType.serial,
   });
 
   /// The name of the method.
@@ -49,6 +51,9 @@
   /// An override for the generated objc selector (ex. "divideNumber:by:").
   String objcSelector;
 
+  /// Specifies how handlers are dispatched with respect to threading.
+  TaskQueueType taskQueueType;
+
   @override
   String toString() {
     final String objcSelectorStr =
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 948c197..107f035 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 = '2.0.0';
+const String pigeonVersion = '2.0.1';
 
 /// 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 9e97488..efa210c 100644
--- a/packages/pigeon/lib/java_generator.dart
+++ b/packages/pigeon/lib/java_generator.dart
@@ -2,10 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:pigeon/functional.dart';
-
 import 'ast.dart';
+import 'functional.dart';
 import 'generator_tools.dart';
+import 'pigeon_lib.dart';
 
 /// Options that control how Java code will be generated.
 class JavaOptions {
@@ -152,11 +152,22 @@
     final String channelName = makeChannelName(api, method);
     indent.write('');
     indent.scoped('{', '}', () {
+      String? taskQueue;
+      if (method.taskQueueType != TaskQueueType.serial) {
+        taskQueue = 'taskQueue';
+        indent.writeln(
+            'BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue();');
+      }
       indent.writeln('BasicMessageChannel<Object> channel =');
       indent.inc();
       indent.inc();
-      indent.writeln(
-          'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec());');
+      indent.write(
+          'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec()');
+      if (taskQueue != null) {
+        indent.addln(', $taskQueue);');
+      } else {
+        indent.addln(');');
+      }
       indent.dec();
       indent.dec();
       indent.write('if (api != null) ');
diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart
index 7839b38..df900d5 100644
--- a/packages/pigeon/lib/objc_generator.dart
+++ b/packages/pigeon/lib/objc_generator.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'package:pigeon/functional.dart';
+import 'package:pigeon/pigeon_lib.dart';
 
 import 'ast.dart';
 import 'generator_tools.dart';
@@ -588,15 +589,20 @@
   assert(api.location == ApiLocation.host);
   final String apiName = _className(options.prefix, api.name);
 
-  void writeChannelAllocation(Method func, String varName) {
+  void writeChannelAllocation(Method func, String varName, String? taskQueue) {
     indent.writeln('FlutterBasicMessageChannel *$varName =');
     indent.inc();
-    indent.writeln('[FlutterBasicMessageChannel');
+    indent.writeln('[[FlutterBasicMessageChannel alloc]');
     indent.inc();
-    indent.writeln('messageChannelWithName:@"${makeChannelName(api, func)}"');
+    indent.writeln('initWithName:@"${makeChannelName(api, func)}"');
     indent.writeln('binaryMessenger:binaryMessenger');
-    indent
-        .writeln('codec:${_getCodecGetterName(options.prefix, api.name)}()];');
+    indent.write('codec:${_getCodecGetterName(options.prefix, api.name)}()');
+    if (taskQueue != null) {
+      indent.addln('');
+      indent.writeln('taskQueue:$taskQueue];');
+    } else {
+      indent.writeln('];');
+    }
     indent.dec();
     indent.dec();
   }
@@ -698,7 +704,13 @@
     for (final Method func in api.methods) {
       indent.write('');
       indent.scoped('{', '}', () {
-        writeChannelAllocation(func, channelName);
+        String? taskQueue;
+        if (func.taskQueueType != TaskQueueType.serial) {
+          taskQueue = 'taskQueue';
+          indent.writeln(
+              'NSObject<FlutterTaskQueue> *$taskQueue = [binaryMessenger makeBackgroundTaskQueue];');
+        }
+        writeChannelAllocation(func, channelName, taskQueue);
         indent.write('if (api) ');
         indent.scoped('{', '}', () {
           writeChannelApiBinding(func, channelName);
diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart
index a53a637..85144d9 100644
--- a/packages/pigeon/lib/pigeon_lib.dart
+++ b/packages/pigeon/lib/pigeon_lib.dart
@@ -94,6 +94,33 @@
   final String value;
 }
 
+/// Type of TaskQueue which determines how handlers are dispatched for
+/// HostApi's.
+enum TaskQueueType {
+  /// Handlers are invoked serially on the default thread. This is the value if
+  /// unspecified.
+  serial,
+
+  /// Handlers are invoked serially on a background thread.
+  serialBackgroundThread,
+
+  // TODO(gaaclarke): Add support for concurrent task queues.
+  // /// Handlers are invoked concurrently on a background thread.
+  // concurrentBackgroundThread,
+}
+
+/// Metadata annotation to control how handlers are dispatched for HostApi's.
+/// Note that the TaskQueue API might not be available on the target version of
+/// Flutter, see also:
+/// https://docs.flutter.dev/development/platform-integration/platform-channels.
+class TaskQueue {
+  /// The constructor for a TaskQueue.
+  const TaskQueue({required this.type});
+
+  /// The type of the TaskQueue.
+  final TaskQueueType type;
+}
+
 /// Represents an error as a result of parsing and generating code.
 class Error {
   /// Parametric constructor for Error.
@@ -507,6 +534,13 @@
           ));
         }
       }
+      if (method.taskQueueType != TaskQueueType.serial &&
+          api.location != ApiLocation.host) {
+        result.add(Error(
+          message: 'Unsupported TaskQueue specification on ${method.name}',
+          lineNumber: _calculateLineNumberNullable(source, method.offset),
+        ));
+      }
     }
   }
 
@@ -757,6 +791,18 @@
     return null;
   }
 
+  T? _stringToEnum<T>(List<T> values, String? str) {
+    if (str == null) {
+      return null;
+    }
+    for (final T value in values) {
+      if (value.toString() == str) {
+        return value;
+      }
+    }
+    return null;
+  }
+
   @override
   Object? visitMethodDeclaration(dart_ast.MethodDeclaration node) {
     final dart_ast.FormalParameterList parameters = node.parameters!;
@@ -770,6 +816,17 @@
             .asNullable<dart_ast.SimpleStringLiteral>()
             ?.value ??
         '';
+    final dart_ast.ArgumentList? taskQueueArguments =
+        _findMetadata(node.metadata, 'TaskQueue')?.arguments;
+    final String? taskQueueTypeName = taskQueueArguments == null
+        ? null
+        : getFirstChildOfType<dart_ast.NamedExpression>(taskQueueArguments)
+            ?.expression
+            .asNullable<dart_ast.PrefixedIdentifier>()
+            ?.name;
+    final TaskQueueType taskQueueType =
+        _stringToEnum(TaskQueueType.values, taskQueueTypeName) ??
+            TaskQueueType.serial;
     if (_currentApi != null) {
       // Methods without named return types aren't supported.
       final dart_ast.TypeAnnotation returnType = node.returnType!;
@@ -785,7 +842,8 @@
           arguments: arguments,
           isAsynchronous: isAsynchronous,
           objcSelector: objcSelector,
-          offset: node.offset));
+          offset: node.offset,
+          taskQueueType: taskQueueType));
     } else if (_currentClass != null) {
       _errors.add(Error(
           message:
diff --git a/packages/pigeon/pigeons/background_platform_channels.dart b/packages/pigeon/pigeons/background_platform_channels.dart
new file mode 100644
index 0000000..c21be6c
--- /dev/null
+++ b/packages/pigeon/pigeons/background_platform_channels.dart
@@ -0,0 +1,11 @@
+// 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.
+
+import 'package:pigeon/pigeon.dart';
+
+@HostApi()
+abstract class Api2Host {
+  @TaskQueue(type: TaskQueueType.serialBackgroundThread)
+  int add(int x, int y);
+}
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
index aea750f..2abcbc5 100644
--- a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
@@ -10,6 +10,7 @@
 		0D02163D27BC7B48009BD76F /* nullable_returns.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */; };
 		0D36469D27C6BE3C0069B7BF /* NullableReturnsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D36469C27C6BE3C0069B7BF /* NullableReturnsTest.m */; };
 		0D3646A027C6DCEC0069B7BF /* MockBinaryMessenger.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D36469F27C6DCEC0069B7BF /* MockBinaryMessenger.m */; };
+		0D21E59A27D0502D0051D07D /* background_platform_channels.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D21E59827D0502D0051D07D /* background_platform_channels.gen.m */; };
 		0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D50127423FF75B100CD5B95 /* RunnerTests.m */; };
 		0D6FD3C526A76D400046D8BD /* primitive.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6FD3C426A76D400046D8BD /* primitive.gen.m */; };
 		0D6FD3C726A777C00046D8BD /* PrimitiveTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6FD3C626A777C00046D8BD /* PrimitiveTest.m */; };
@@ -72,6 +73,8 @@
 		0D36469C27C6BE3C0069B7BF /* NullableReturnsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NullableReturnsTest.m; sourceTree = "<group>"; };
 		0D36469E27C6DCEC0069B7BF /* MockBinaryMessenger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockBinaryMessenger.h; sourceTree = "<group>"; };
 		0D36469F27C6DCEC0069B7BF /* MockBinaryMessenger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockBinaryMessenger.m; sourceTree = "<group>"; };
+		0D21E59827D0502D0051D07D /* background_platform_channels.gen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = background_platform_channels.gen.m; sourceTree = "<group>"; };
+		0D21E59927D0502D0051D07D /* background_platform_channels.gen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = background_platform_channels.gen.h; sourceTree = "<group>"; };
 		0D50127223FF75B100CD5B95 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		0D50127423FF75B100CD5B95 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = "<group>"; };
 		0D50127623FF75B100CD5B95 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -204,6 +207,8 @@
 		97C146F01CF9000F007C117D /* Runner */ = {
 			isa = PBXGroup;
 			children = (
+				0D21E59927D0502D0051D07D /* background_platform_channels.gen.h */,
+				0D21E59827D0502D0051D07D /* background_platform_channels.gen.m */,
 				0D02163B27BC7B48009BD76F /* nullable_returns.gen.h */,
 				0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */,
 				0DBD8C40279B741800E4FDBA /* non_null_fields.gen.h */,
@@ -424,6 +429,7 @@
 				0DD2E6BC2684031300A7D764 /* host2flutter.gen.m in Sources */,
 				0DA5DFD626CC39D600D2354B /* multiple_arity.gen.m in Sources */,
 				0DD2E6BE2684031300A7D764 /* message.gen.m in Sources */,
+				0D21E59A27D0502D0051D07D /* background_platform_channels.gen.m in Sources */,
 				0DD2E6BA2684031300A7D764 /* void_arg_host.gen.m in Sources */,
 				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
 				0D02163D27BC7B48009BD76F /* nullable_returns.gen.m in Sources */,
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index 12f922e..098cfac 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/main/packages/pigeon
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon
-version: 2.0.0 # This must match the version in lib/generator_tools.dart
+version: 2.0.1 # This must match the version in lib/generator_tools.dart
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh
index 60f3971..91d86ed 100755
--- a/packages/pigeon/run_tests.sh
+++ b/packages/pigeon/run_tests.sh
@@ -235,6 +235,7 @@
   gen_ios_unittests_code ./pigeons/all_void.dart ""
   gen_ios_unittests_code ./pigeons/all_datatypes.dart ""
   gen_ios_unittests_code ./pigeons/async_handlers.dart ""
+  gen_ios_unittests_code ./pigeons/background_platform_channels.dart "BC"
   gen_ios_unittests_code ./pigeons/enum.dart "AC"
   gen_ios_unittests_code ./pigeons/host2flutter.dart ""
   gen_ios_unittests_code ./pigeons/list.dart "LST"
@@ -294,6 +295,7 @@
   gen_android_unittests_code ./pigeons/all_void.dart AllVoid
   gen_android_unittests_code ./pigeons/android_unittests.dart Pigeon
   gen_android_unittests_code ./pigeons/async_handlers.dart AsyncHandlers
+  gen_android_unittests_code ./pigeons/background_platform_channels.dart BackgroundPlatformChannels
   gen_android_unittests_code ./pigeons/enum.dart Enum
   gen_android_unittests_code ./pigeons/host2flutter.dart Host2Flutter
   gen_android_unittests_code ./pigeons/java_double_host_api.dart JavaDoubleHostApi
diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart
index 57c4ef9..f65fe58 100644
--- a/packages/pigeon/test/java_generator_test.dart
+++ b/packages/pigeon/test/java_generator_test.dart
@@ -4,6 +4,7 @@
 
 import 'package:pigeon/ast.dart';
 import 'package:pigeon/java_generator.dart';
+import 'package:pigeon/pigeon.dart';
 import 'package:test/test.dart';
 
 void main() {
@@ -983,4 +984,39 @@
         contains(
             'public void doit(@Nullable Long fooArg, Reply<Void> callback) {'));
   });
+
+  test('background platform channel', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration.voidDeclaration(),
+              arguments: <NamedType>[
+                NamedType(
+                    name: 'foo',
+                    type: const TypeDeclaration(
+                      baseName: 'int',
+                      isNullable: true,
+                    )),
+              ],
+              taskQueueType: TaskQueueType.serialBackgroundThread)
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    const JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            'BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue();'));
+    expect(
+        code,
+        contains(
+            'new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.Api.doit", getCodec(), taskQueue)'));
+  });
 }
diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart
index 7044ecf..590d710 100644
--- a/packages/pigeon/test/objc_generator_test.dart
+++ b/packages/pigeon/test/objc_generator_test.dart
@@ -4,6 +4,7 @@
 
 import 'package:pigeon/ast.dart';
 import 'package:pigeon/objc_generator.dart';
+import 'package:pigeon/pigeon_lib.dart';
 import 'package:test/test.dart';
 
 void main() {
@@ -1731,4 +1732,31 @@
       expect(code, contains('- (void)doitFoo:(nullable NSNumber *)arg_foo'));
     }
   });
+
+  test('background platform channel', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                baseName: 'int',
+                isNullable: true,
+              ),
+              arguments: <NamedType>[],
+              taskQueueType: TaskQueueType.serialBackgroundThread)
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(const ObjcOptions(), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            'NSObject<FlutterTaskQueue> *taskQueue = [binaryMessenger makeBackgroundTaskQueue];'));
+    expect(code, contains('taskQueue:taskQueue'));
+  });
 }
diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart
index 9922bc5..6d6447c 100644
--- a/packages/pigeon/test/pigeon_lib_test.dart
+++ b/packages/pigeon/test/pigeon_lib_test.dart
@@ -1041,4 +1041,48 @@
     expect(
         results.root.apis[0].methods[0].arguments[0].type.isNullable, isTrue);
   });
+
+  test('task queue specified', () {
+    const String code = '''
+@HostApi()
+abstract class Api {
+  @TaskQueue(type: TaskQueueType.serialBackgroundThread)
+  int? calc();
+}
+''';
+
+    final ParseResults results = _parseSource(code);
+    expect(results.errors.length, 0);
+    expect(results.root.apis[0].methods[0].taskQueueType,
+        equals(TaskQueueType.serialBackgroundThread));
+  });
+
+  test('task queue unspecified', () {
+    const String code = '''
+@HostApi()
+abstract class Api {
+  int? calc();
+}
+''';
+
+    final ParseResults results = _parseSource(code);
+    expect(results.errors.length, 0);
+    expect(results.root.apis[0].methods[0].taskQueueType,
+        equals(TaskQueueType.serial));
+  });
+
+  test('unsupported task queue on FlutterApi', () {
+    const String code = '''
+@FlutterApi()
+abstract class Api {
+  @TaskQueue(type: TaskQueueType.serialBackgroundThread)
+  int? calc();
+}
+''';
+
+    final ParseResults results = _parseSource(code);
+    expect(results.errors.length, 1);
+    expect(results.errors[0].message,
+        contains('Unsupported TaskQueue specification'));
+  });
 }