[jnigen] Improve coverage (https://github.com/dart-lang/jnigen/issues/134)

diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index a57feba..acfea75 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -87,6 +87,12 @@
           sudo apt-get install -y clang-format
       - name: Install dependencies
         run: dart pub get
+      - name: build in_app_java APK
+        run: flutter build apk --target-platform=android-arm64
+        working-directory: ./pkgs/jnigen/example/in_app_java
+      - name: build notification_plugin example APK
+        run: flutter build apk --target-platform=android-arm64
+        working-directory: ./pkgs/jnigen/example/notification_plugin/example
       - name: Run VM tests
         run: dart test --platform vm
       - name: Install coverage
@@ -245,6 +251,12 @@
       - run: Add-Content $env:GITHUB_PATH "$env:JAVA_HOME\bin\server"
       - run: dart pub get
       - run: dart run jnigen:setup
+      - name: build in_app_java APK
+        run: flutter build apk --target-platform=android-arm64
+        working-directory: ./pkgs/jnigen/example/in_app_java
+      - name: build notification_plugin example APK
+        run: flutter build apk --target-platform=android-arm64
+        working-directory: ./pkgs/jnigen/example/notification_plugin/example
       - run: dart test
 
   test_jni_macos_minimal:
@@ -359,63 +371,6 @@
       - run: flutter pub get
       - run: flutter build apk
 
-  build_notification_plugin_example:
-    runs-on: ubuntu-latest
-    defaults:
-      run:
-        working-directory: ./pkgs/jnigen/example/notification_plugin
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-java@v2
-        with:
-          distribution: 'zulu'
-          java-version: '11'
-      - uses: subosito/flutter-action@v2
-        with:
-          channel: 'stable'
-          cache: true
-          cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
-      - run: flutter pub get
-      - run: flutter analyze
-      - run: flutter build apk
-        working-directory: ./pkgs/jnigen/example/notification_plugin/example
-      - name: re-generate bindings
-        run: flutter pub run jnigen -Doutput.dart.path=_temp.dart -Doutput.c.path=_c/ --config jnigen.yaml
-      - name: compare generated dart bindings
-        run: diff lib/notifications.dart _temp.dart
-      - name: compare generated C bindings
-        run: diff -r src/ _c
-
-  build_in_app_java_example:
-    runs-on: ubuntu-latest
-    defaults:
-      run:
-        working-directory: ./pkgs/jnigen/example/in_app_java
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-java@v2
-        with:
-          distribution: 'zulu'
-          java-version: '11'
-      - uses: subosito/flutter-action@v2
-        with:
-          channel: 'stable'
-          cache: true
-          cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
-      - name: install clang tools
-        run: |
-          sudo apt-get update -y
-          sudo apt-get install -y clang-format
-      - run: flutter pub get
-      - run: flutter analyze
-      - run: flutter build apk
-      - name: re-generate bindings
-        run: flutter pub run jnigen -Doutput.dart.path=_temp.dart -Doutput.c.path=_c/ --config jnigen.yaml
-      - name: compare generated dart bindings
-        run: diff lib/android_utils.dart _temp.dart
-      - name: compare generated C bindings
-        run: diff -r src/android_utils _c
-
   run_pdfbox_example_linux:
     runs-on: ubuntu-latest
     defaults:
diff --git a/pkgs/jnigen/bin/jnigen.dart b/pkgs/jnigen/bin/jnigen.dart
index 01bae85..a5065de 100644
--- a/pkgs/jnigen/bin/jnigen.dart
+++ b/pkgs/jnigen/bin/jnigen.dart
@@ -3,8 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:jnigen/jnigen.dart';
+import 'package:jnigen/src/logging/logging.dart';
 
 void main(List<String> args) async {
-  final config = Config.parseArgs(args);
+  Config config;
+  try {
+    config = Config.parseArgs(args);
+  } on ConfigException catch (e) {
+    log.fatal(e);
+    return;
+  }
   await generateJniBindings(config);
 }
diff --git a/pkgs/jnigen/lib/src/bindings/common.dart b/pkgs/jnigen/lib/src/bindings/common.dart
index ab1bc7f..61931b8 100644
--- a/pkgs/jnigen/lib/src/bindings/common.dart
+++ b/pkgs/jnigen/lib/src/bindings/common.dart
@@ -207,7 +207,7 @@
       case Kind.typeVariable:
       case Kind.wildcard:
         throw SkipException(
-            'Generic type parameters are not supported', t.toJson());
+            'Generic type parameters are not supported', t.name);
       case Kind.array:
       case Kind.declared:
         return voidPointer;
diff --git a/pkgs/jnigen/lib/src/config/config.dart b/pkgs/jnigen/lib/src/config/config.dart
index 56e09d0..c4168d8 100644
--- a/pkgs/jnigen/lib/src/config/config.dart
+++ b/pkgs/jnigen/lib/src/config/config.dart
@@ -2,487 +2,5 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'dart:io';
-
-import 'package:jnigen/src/elements/elements.dart';
-
-import 'yaml_reader.dart';
-import 'filters.dart';
-
-import 'package:logging/logging.dart';
-
-/// Configuration for dependencies to be downloaded using maven.
-///
-/// Dependency names should be listed in groupId:artifactId:version format.
-/// For [sourceDeps], sources will be unpacked to [sourceDir] root and JAR files
-/// will also be downloaded. For the packages in jarOnlyDeps, only JAR files
-/// will be downloaded.
-///
-/// When passed as a parameter to [Config], the downloaded sources and
-/// JAR files will be automatically added to source path and class path
-/// respectively.
-class MavenDownloads {
-  static const defaultMavenSourceDir = 'mvn_java';
-  static const defaultMavenJarDir = 'mvn_jar';
-
-  MavenDownloads({
-    this.sourceDeps = const [],
-    // ASK: Should this be changed to a gitignore'd directory like build ?
-    this.sourceDir = defaultMavenSourceDir,
-    this.jarOnlyDeps = const [],
-    this.jarDir = defaultMavenJarDir,
-  });
-  List<String> sourceDeps;
-  String sourceDir;
-  List<String> jarOnlyDeps;
-  String jarDir;
-}
-
-/// Configuration for Android SDK sources and stub JAR files.
-///
-/// The SDK directories for platform stub JARs and sources are searched in the
-/// same order in which [versions] are specified.
-///
-/// If [includeSources] is true, `jnigen` searches for Android SDK sources
-/// as well in the SDK directory and adds them to the source path.
-///
-/// If [addGradleDeps] is true, a gradle stub is run in order to collect the
-/// actual compile classpath of the `android/` subproject.
-/// This will fail if there was no previous build of the project, or if a
-/// `clean` task was run either through flutter or gradle wrapper. In such case,
-/// it's required to run `flutter build apk` & retry running `jnigen`.
-///
-/// A configuration is invalid if [versions] is unspecified or empty, and
-/// [addGradleDeps] is also false. If [sdkRoot] is not specified but versions is
-/// specified, an attempt is made to find out SDK installation directory using
-/// environment variable `ANDROID_SDK_ROOT` if it's defined, else an error
-/// will be thrown.
-class AndroidSdkConfig {
-  AndroidSdkConfig({
-    this.versions,
-    this.sdkRoot,
-    this.includeSources = false,
-    this.addGradleDeps = false,
-    this.androidExample,
-  }) {
-    if (versions != null && sdkRoot == null) {
-      throw ArgumentError("No SDK Root specified for finding Android SDK "
-          "from version priority list $versions");
-    }
-    if (versions == null && !addGradleDeps) {
-      throw ArgumentError('Neither any SDK versions nor `addGradleDeps` '
-          'is specified. Unable to find Android libraries.');
-    }
-  }
-
-  /// Versions of android SDK to search for, in decreasing order of preference.
-  List<int>? versions;
-
-  /// Root of Android SDK installation, this should be normally given on
-  /// command line or by setting `ANDROID_SDK_ROOT`, since this varies from
-  /// system to system.
-  String? sdkRoot;
-
-  /// Include downloaded android SDK sources in source path.
-  bool includeSources;
-
-  /// Attempt to determine exact compile time dependencies by running a gradle
-  /// stub in android subproject of this project.
-  ///
-  /// An Android build must have happened before we are able to obtain classpath
-  /// of Gradle dependencies. Run `flutter build apk` before running a jnigen
-  /// script with this option.
-  ///
-  /// For the same reason, if the flutter project is a plugin instead of
-  /// application, it's not possible to determine the build classpath directly.
-  /// Please provide [androidExample] pointing to an example application in
-  /// that case.
-  bool addGradleDeps;
-
-  /// Relative path to example application which will be used to determine
-  /// compile time classpath using a gradle stub. For most Android plugin
-  /// packages, 'example' will be the name of example application created inside
-  /// the package. This example should be built once before using this option,
-  /// so that gradle would have resolved all the dependencies.
-  String? androidExample;
-}
-
-/// Additional options to pass to the summary generator component.
-class SummarizerOptions {
-  SummarizerOptions(
-      {this.extraArgs = const [], this.workingDirectory, this.backend});
-  List<String> extraArgs;
-  Uri? workingDirectory;
-  String? backend;
-}
-
-/// Backend for reading summary of Java libraries
-enum SummarizerBackend {
-  /// Generate Java API summaries using JARs in provided `classPath`s.
-  asm,
-
-  /// Generate Java API summaries using source files in provided `sourcePath`s.
-  doclet,
-}
-
-T _getEnumValueFromString<T>(
-    Map<String, T> values, String? name, T defaultVal) {
-  if (name == null) return defaultVal;
-  final value = values[name];
-  if (value == null) throw ArgumentError('Got: $name, allowed: ${values.keys}');
-  return value;
-}
-
-void _ensureIsDirectory(String name, Uri path) {
-  if (!path.toFilePath().endsWith(Platform.pathSeparator)) {
-    throw ArgumentError('$name must be a directory path. If using YAML '
-        'config, please ensure the path ends with a slash (/).');
-  }
-}
-
-enum OutputStructure { packageStructure, singleFile }
-
-OutputStructure getOutputStructure(String? name, OutputStructure defaultVal) {
-  const values = {
-    'package_structure': OutputStructure.packageStructure,
-    'single_file': OutputStructure.singleFile,
-  };
-  return _getEnumValueFromString(values, name, defaultVal);
-}
-
-enum BindingsType { cBased, dartOnly }
-
-BindingsType getBindingsType(String? name, BindingsType defaultVal) {
-  const values = {
-    'c_based': BindingsType.cBased,
-    'dart_only': BindingsType.dartOnly,
-  };
-  return _getEnumValueFromString(values, name, defaultVal);
-}
-
-class CCodeOutputConfig {
-  CCodeOutputConfig({
-    required this.path,
-    required this.libraryName,
-    this.subdir,
-  }) {
-    _ensureIsDirectory('C output path', path);
-    if (subdir != null) {
-      _ensureIsDirectory('C subdirectory', path.resolve(subdir!));
-    }
-  }
-
-  /// Directory to write JNI C Bindings, in C+Dart mode.
-  ///
-  /// Strictly speaking, this is the root to place the `CMakeLists.txt` file
-  /// for the generated C bindings. It may be desirable to use the [subdir]
-  /// options to write C files to a subdirectory of [path]. For instance,
-  /// when generated code is required to be in `third_party` directory.
-  Uri path;
-
-  /// Name of generated library in CMakeLists.txt configuration.
-  ///
-  /// This will also determine the name of shared object file.
-  String libraryName;
-
-  /// Subfolder relative to [path] to write generated C code.
-  String? subdir;
-}
-
-class DartCodeOutputConfig {
-  DartCodeOutputConfig({
-    required this.path,
-    this.structure = OutputStructure.packageStructure,
-  }) {
-    if (structure == OutputStructure.singleFile) {
-      if (!path.toFilePath().endsWith('.dart')) {
-        throw ArgumentError(
-            'output path must end with ".dart" in single file mode');
-      }
-    } else {
-      _ensureIsDirectory('Dart output path', path);
-    }
-  }
-
-  /// Path to write generated Dart bindings.
-  Uri path;
-
-  /// File structure of the generated Dart bindings.
-  OutputStructure structure;
-}
-
-class OutputConfig {
-  OutputConfig({
-    this.bindingsType = BindingsType.cBased,
-    this.cConfig,
-    required this.dartConfig,
-  }) {
-    if (bindingsType == BindingsType.cBased && cConfig == null) {
-      throw ArgumentError("C output config must be provided!");
-    }
-  }
-  BindingsType bindingsType;
-  DartCodeOutputConfig dartConfig;
-  CCodeOutputConfig? cConfig;
-}
-
-class BindingExclusions {
-  BindingExclusions({this.methods, this.fields, this.classes});
-  MethodFilter? methods;
-  FieldFilter? fields;
-  ClassFilter? classes;
-}
-
-/// Configuration for jnigen binding generation.
-class Config {
-  Config({
-    required this.outputConfig,
-    required this.classes,
-    this.exclude,
-    this.sourcePath,
-    this.classPath,
-    this.preamble,
-    this.importMap,
-    this.androidSdkConfig,
-    this.mavenDownloads,
-    this.summarizerOptions,
-    this.logLevel = Level.INFO,
-    this.dumpJsonTo,
-  });
-
-  /// Output configuration for generated bindings
-  OutputConfig outputConfig;
-
-  /// List of classes or packages for which bindings have to be generated.
-  ///
-  /// The names must be fully qualified, and it's assumed that the directory
-  /// structure corresponds to package naming. For example, com.abc.MyClass
-  /// should be resolvable as `com/abc/MyClass.java` from one of the provided
-  /// source paths. Same applies if ASM backend is used, except that the file
-  /// name suffix is `.class`.
-  List<String> classes;
-
-  /// Methods and fields to be excluded from generated bindings.
-  final BindingExclusions? exclude;
-
-  /// Paths to search for java source files.
-  ///
-  /// If a source package is downloaded through [mavenDownloads] option,
-  /// the corresponding source folder is automatically added and does not
-  /// need to be explicitly specified.
-  List<Uri>? sourcePath;
-
-  /// class path for scanning java libraries. If [backend] is `asm`, the
-  /// specified classpath is used to search for [classes], otherwise it's
-  /// merely used by the doclet API to find transitively referenced classes,
-  /// but not the specified classes / packages themselves.
-  List<Uri>? classPath;
-
-  /// Common text to be pasted on top of generated C and Dart files.
-  final String? preamble;
-
-  /// Additional java package -> dart package mappings (Experimental).
-  ///
-  /// a mapping com.abc.package -> 'package:my_package.dart/my_import.dart'
-  /// in this configuration suggests that any reference to a type from
-  /// com.abc.package shall resolve to an import of 'package:my_package.dart'.
-  ///
-  /// This can be as granular
-  /// `com.abc.package.Class -> 'package:abc/abc.dart'`
-  /// or coarse
-  /// `com.abc.package` -> 'package:abc/abc.dart'`
-  final Map<String, String>? importMap;
-
-  /// Configuration to search for Android SDK libraries (Experimental).
-  final AndroidSdkConfig? androidSdkConfig;
-
-  /// Configuration for auto-downloading JAR / source packages using maven,
-  /// along with their transitive dependencies.
-  final MavenDownloads? mavenDownloads;
-
-  /// Additional options for the summarizer component
-  final SummarizerOptions? summarizerOptions;
-
-  /// Log verbosity. The possible values in decreasing order of verbosity
-  /// are verbose > debug > info > warning > error. Defaults to [LogLevel.info]
-  Level logLevel = Level.INFO;
-
-  /// File to which JSON summary is written before binding generation.
-  final String? dumpJsonTo;
-
-  static final _levels = Map.fromEntries(
-      Level.LEVELS.map((l) => MapEntry(l.name.toLowerCase(), l)));
-  static List<Uri>? _toUris(List<String>? paths) =>
-      paths?.map(Uri.file).toList();
-
-  static Config parseArgs(List<String> args) {
-    final prov = YamlReader.parseArgs(args);
-    final List<String> missingValues = [];
-    T must<T>(T? Function(String) f, T ifNull, String property) {
-      final res = f(property);
-      if (res == null) {
-        missingValues.add(property);
-        return ifNull;
-      }
-      return res;
-    }
-
-    Uri? directoryUri(String? path) =>
-        path != null ? Uri.directory(path) : null;
-
-    MemberFilter<T>? regexFilter<T extends ClassMember>(String property) {
-      final exclusions = prov.getStringList(property);
-      if (exclusions == null) return null;
-      final List<MemberFilter<T>> filters = [];
-      for (var exclusion in exclusions) {
-        final split = exclusion.split('#');
-        if (split.length != 2) {
-          throw FormatException('Error parsing exclusion: "$exclusion": '
-              'expected to be in binaryName#member format.');
-        }
-        filters.add(MemberNameFilter<T>.exclude(
-          RegExp(split[0]),
-          RegExp(split[1]),
-        ));
-      }
-      return CombinedMemberFilter<T>(filters);
-    }
-
-    String? getSdkRoot() {
-      final root = prov.getString(_Props.androidSdkRoot) ??
-          Platform.environment['ANDROID_SDK_ROOT'];
-      return root;
-    }
-
-    Level logLevelFromString(String? levelName) {
-      if (levelName == null) return Level.INFO;
-      final level = _levels[levelName.toLowerCase()];
-      if (level == null) {
-        throw ConfigError('Not a valid logging level: $levelName');
-      }
-      return level;
-    }
-
-    final config = Config(
-      sourcePath: _toUris(prov.getStringList(_Props.sourcePath)),
-      classPath: _toUris(prov.getStringList(_Props.classPath)),
-      classes: must(prov.getStringList, [], _Props.classes),
-      summarizerOptions: SummarizerOptions(
-        extraArgs: prov.getStringList(_Props.summarizerArgs) ?? const [],
-        backend: prov.getString(_Props.backend),
-        workingDirectory:
-            directoryUri(prov.getString(_Props.summarizerWorkingDir)),
-      ),
-      exclude: BindingExclusions(
-        methods: regexFilter<Method>(_Props.excludeMethods),
-        fields: regexFilter<Field>(_Props.excludeFields),
-      ),
-      outputConfig: OutputConfig(
-        bindingsType: getBindingsType(
-          prov.getString(_Props.bindingsType),
-          BindingsType.cBased,
-        ),
-        cConfig: prov.hasValue(_Props.cCodeOutputConfig)
-            ? CCodeOutputConfig(
-                libraryName: must(prov.getString, '', _Props.libraryName),
-                path: Uri.file(must(prov.getString, '.', _Props.cRoot)),
-                subdir: prov.getString(_Props.cSubdir),
-              )
-            : null,
-        dartConfig: DartCodeOutputConfig(
-          path: Uri.file(must(prov.getString, '.', _Props.dartRoot)),
-          structure: getOutputStructure(
-            prov.getString(_Props.outputStructure),
-            OutputStructure.packageStructure,
-          ),
-        ),
-      ),
-      preamble: prov.getString(_Props.preamble),
-      importMap: prov.getStringMap(_Props.importMap),
-      mavenDownloads: prov.hasValue(_Props.mavenDownloads)
-          ? MavenDownloads(
-              sourceDeps: prov.getStringList(_Props.sourceDeps) ?? const [],
-              sourceDir: prov.getString(_Props.mavenSourceDir) ??
-                  MavenDownloads.defaultMavenSourceDir,
-              jarOnlyDeps: prov.getStringList(_Props.jarOnlyDeps) ?? const [],
-              jarDir: prov.getString(_Props.mavenJarDir) ??
-                  MavenDownloads.defaultMavenJarDir,
-            )
-          : null,
-      androidSdkConfig: prov.hasValue(_Props.androidSdkConfig)
-          ? AndroidSdkConfig(
-              versions: prov
-                  .getStringList(_Props.androidSdkVersions)
-                  ?.map(int.parse)
-                  .toList(),
-              sdkRoot: getSdkRoot(),
-              includeSources:
-                  prov.getBool(_Props.includeAndroidSources) ?? false,
-              addGradleDeps: prov.getBool(_Props.addGradleDeps) ?? false,
-              androidExample: prov.getString(_Props.androidExample),
-            )
-          : null,
-      logLevel: logLevelFromString(
-        prov.getOneOf(
-          _Props.logLevel,
-          _levels.keys.toSet(),
-        ),
-      ),
-    );
-    if (missingValues.isNotEmpty) {
-      stderr.write('Following config values are required but not provided\n'
-          'Please provide these properties through YAML '
-          'or use the command line switch -D<property_name>=<value>.\n');
-      for (var missing in missingValues) {
-        stderr.writeln('* $missing');
-      }
-      if (missingValues.contains(_Props.androidSdkRoot)) {
-        stderr.writeln('Please specify ${_Props.androidSdkRoot} through '
-            'command line or ensure that the ANDROID_SDK_ROOT environment '
-            'variable is set.');
-      }
-      exit(1);
-    }
-    return config;
-  }
-}
-
-class _Props {
-  static const summarizer = 'summarizer';
-  static const summarizerArgs = '$summarizer.extra_args';
-  static const summarizerWorkingDir = '$summarizer.working_dir';
-  static const backend = '$summarizer.backend';
-
-  static const sourcePath = 'source_path';
-  static const classPath = 'class_path';
-  static const classes = 'classes';
-  static const exclude = 'exclude';
-  static const excludeMethods = '$exclude.methods';
-  static const excludeFields = '$exclude.fields';
-
-  static const importMap = 'import_map';
-  static const outputConfig = 'output';
-  static const bindingsType = '$outputConfig.bindings_type';
-  static const cCodeOutputConfig = '$outputConfig.c';
-  static const dartCodeOutputConfig = '$outputConfig.dart';
-  static const cRoot = '$cCodeOutputConfig.path';
-  static const cSubdir = '$cCodeOutputConfig.subdir';
-  static const dartRoot = '$dartCodeOutputConfig.path';
-  static const outputStructure = '$dartCodeOutputConfig.structure';
-  static const libraryName = '$cCodeOutputConfig.library_name';
-  static const preamble = 'preamble';
-  static const logLevel = 'log_level';
-
-  static const mavenDownloads = 'maven_downloads';
-  static const sourceDeps = '$mavenDownloads.source_deps';
-  static const mavenSourceDir = '$mavenDownloads.source_dir';
-  static const jarOnlyDeps = '$mavenDownloads.jar_only_deps';
-  static const mavenJarDir = '$mavenDownloads.jar_dir';
-
-  static const androidSdkConfig = 'android_sdk_config';
-  static const androidSdkRoot = '$androidSdkConfig.sdk_root';
-  static const androidSdkVersions = '$androidSdkConfig.versions';
-  static const includeAndroidSources = '$androidSdkConfig.include_sources';
-  static const addGradleDeps = '$androidSdkConfig.add_gradle_deps';
-  static const androidExample = '$androidSdkConfig.android_example';
-}
+export 'config_types.dart';
+export 'config_exception.dart';
diff --git a/pkgs/jnigen/lib/src/config/config_exception.dart b/pkgs/jnigen/lib/src/config/config_exception.dart
new file mode 100644
index 0000000..d3518cc
--- /dev/null
+++ b/pkgs/jnigen/lib/src/config/config_exception.dart
@@ -0,0 +1,12 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Exception thrown when a configuration value is invalid.
+class ConfigException implements Exception {
+  ConfigException(this.message);
+  String message;
+
+  @override
+  String toString() => 'Error parsing configuration: $message';
+}
diff --git a/pkgs/jnigen/lib/src/config/config_types.dart b/pkgs/jnigen/lib/src/config/config_types.dart
new file mode 100644
index 0000000..c32f824
--- /dev/null
+++ b/pkgs/jnigen/lib/src/config/config_types.dart
@@ -0,0 +1,494 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:jnigen/src/elements/elements.dart';
+
+import 'yaml_reader.dart';
+import 'filters.dart';
+import 'config_exception.dart';
+
+import 'package:logging/logging.dart';
+
+/// Configuration for dependencies to be downloaded using maven.
+///
+/// Dependency names should be listed in groupId:artifactId:version format.
+/// For [sourceDeps], sources will be unpacked to [sourceDir] root and JAR files
+/// will also be downloaded. For the packages in jarOnlyDeps, only JAR files
+/// will be downloaded.
+///
+/// When passed as a parameter to [Config], the downloaded sources and
+/// JAR files will be automatically added to source path and class path
+/// respectively.
+class MavenDownloads {
+  static const defaultMavenSourceDir = 'mvn_java';
+  static const defaultMavenJarDir = 'mvn_jar';
+
+  MavenDownloads({
+    this.sourceDeps = const [],
+    // ASK: Should this be changed to a gitignore'd directory like build ?
+    this.sourceDir = defaultMavenSourceDir,
+    this.jarOnlyDeps = const [],
+    this.jarDir = defaultMavenJarDir,
+  });
+  List<String> sourceDeps;
+  String sourceDir;
+  List<String> jarOnlyDeps;
+  String jarDir;
+}
+
+/// Configuration for Android SDK sources and stub JAR files.
+///
+/// The SDK directories for platform stub JARs and sources are searched in the
+/// same order in which [versions] are specified.
+///
+/// If [addGradleDeps] is true, a gradle stub is run in order to collect the
+/// actual compile classpath of the `android/` subproject.
+/// This will fail if there was no previous build of the project, or if a
+/// `clean` task was run either through flutter or gradle wrapper. In such case,
+/// it's required to run `flutter build apk` & retry running `jnigen`.
+///
+/// A configuration is invalid if [versions] is unspecified or empty, and
+/// [addGradleDeps] is also false. If [sdkRoot] is not specified but versions is
+/// specified, an attempt is made to find out SDK installation directory using
+/// environment variable `ANDROID_SDK_ROOT` if it's defined, else an error
+/// will be thrown.
+class AndroidSdkConfig {
+  AndroidSdkConfig({
+    this.versions,
+    this.sdkRoot,
+    this.addGradleDeps = false,
+    this.androidExample,
+  }) {
+    if (versions != null && sdkRoot == null) {
+      throw ConfigException("No SDK Root specified for finding Android SDK "
+          "from version priority list $versions");
+    }
+    if (versions == null && !addGradleDeps) {
+      throw ConfigException('Neither any SDK versions nor `addGradleDeps` '
+          'is specified. Unable to find Android libraries.');
+    }
+  }
+
+  /// Versions of android SDK to search for, in decreasing order of preference.
+  List<int>? versions;
+
+  /// Root of Android SDK installation, this should be normally given on
+  /// command line or by setting `ANDROID_SDK_ROOT`, since this varies from
+  /// system to system.
+  String? sdkRoot;
+
+  /// Attempt to determine exact compile time dependencies by running a gradle
+  /// stub in android subproject of this project.
+  ///
+  /// An Android build must have happened before we are able to obtain classpath
+  /// of Gradle dependencies. Run `flutter build apk` before running a jnigen
+  /// script with this option.
+  ///
+  /// For the same reason, if the flutter project is a plugin instead of
+  /// application, it's not possible to determine the build classpath directly.
+  /// Please provide [androidExample] pointing to an example application in
+  /// that case.
+  bool addGradleDeps;
+
+  /// Relative path to example application which will be used to determine
+  /// compile time classpath using a gradle stub. For most Android plugin
+  /// packages, 'example' will be the name of example application created inside
+  /// the package. This example should be built once before using this option,
+  /// so that gradle would have resolved all the dependencies.
+  String? androidExample;
+}
+
+/// Additional options to pass to the summary generator component.
+class SummarizerOptions {
+  SummarizerOptions(
+      {this.extraArgs = const [], this.workingDirectory, this.backend});
+  List<String> extraArgs;
+  Uri? workingDirectory;
+  String? backend;
+}
+
+/// Backend for reading summary of Java libraries
+enum SummarizerBackend {
+  /// Generate Java API summaries using JARs in provided `classPath`s.
+  asm,
+
+  /// Generate Java API summaries using source files in provided `sourcePath`s.
+  doclet,
+}
+
+T _getEnumValueFromString<T>(
+    Map<String, T> values, String? name, T defaultVal) {
+  if (name == null) return defaultVal;
+  final value = values[name];
+  if (value == null) {
+    throw ConfigException('Got: $name, allowed: ${values.keys}');
+  }
+  return value;
+}
+
+void _ensureIsDirectory(String name, Uri path) {
+  if (!path.toFilePath().endsWith(Platform.pathSeparator)) {
+    throw ConfigException('$name must be a directory path. If using YAML '
+        'config, please ensure the path ends with a slash (/).');
+  }
+}
+
+enum OutputStructure { packageStructure, singleFile }
+
+OutputStructure getOutputStructure(String? name, OutputStructure defaultVal) {
+  const values = {
+    'package_structure': OutputStructure.packageStructure,
+    'single_file': OutputStructure.singleFile,
+  };
+  return _getEnumValueFromString(values, name, defaultVal);
+}
+
+enum BindingsType { cBased, dartOnly }
+
+BindingsType getBindingsType(String? name, BindingsType defaultVal) {
+  const values = {
+    'c_based': BindingsType.cBased,
+    'dart_only': BindingsType.dartOnly,
+  };
+  return _getEnumValueFromString(values, name, defaultVal);
+}
+
+class CCodeOutputConfig {
+  CCodeOutputConfig({
+    required this.path,
+    required this.libraryName,
+    this.subdir,
+  }) {
+    _ensureIsDirectory('C output path', path);
+    if (subdir != null) {
+      _ensureIsDirectory('C subdirectory', path.resolve(subdir!));
+    }
+  }
+
+  /// Directory to write JNI C Bindings, in C+Dart mode.
+  ///
+  /// Strictly speaking, this is the root to place the `CMakeLists.txt` file
+  /// for the generated C bindings. It may be desirable to use the [subdir]
+  /// options to write C files to a subdirectory of [path]. For instance,
+  /// when generated code is required to be in `third_party` directory.
+  Uri path;
+
+  /// Name of generated library in CMakeLists.txt configuration.
+  ///
+  /// This will also determine the name of shared object file.
+  String libraryName;
+
+  /// Subfolder relative to [path] to write generated C code.
+  String? subdir;
+}
+
+class DartCodeOutputConfig {
+  DartCodeOutputConfig({
+    required this.path,
+    this.structure = OutputStructure.packageStructure,
+  }) {
+    if (structure == OutputStructure.singleFile) {
+      if (!path.toFilePath().endsWith('.dart')) {
+        throw ConfigException(
+            'output path must end with ".dart" in single file mode');
+      }
+    } else {
+      _ensureIsDirectory('Dart output path', path);
+    }
+  }
+
+  /// Path to write generated Dart bindings.
+  Uri path;
+
+  /// File structure of the generated Dart bindings.
+  OutputStructure structure;
+}
+
+class OutputConfig {
+  OutputConfig({
+    this.bindingsType = BindingsType.cBased,
+    this.cConfig,
+    required this.dartConfig,
+  }) {
+    if (bindingsType == BindingsType.cBased && cConfig == null) {
+      throw ConfigException("C output config must be provided!");
+    }
+  }
+  BindingsType bindingsType;
+  DartCodeOutputConfig dartConfig;
+  CCodeOutputConfig? cConfig;
+}
+
+class BindingExclusions {
+  BindingExclusions({this.methods, this.fields, this.classes});
+  MethodFilter? methods;
+  FieldFilter? fields;
+  ClassFilter? classes;
+}
+
+/// Configuration for jnigen binding generation.
+class Config {
+  Config({
+    required this.outputConfig,
+    required this.classes,
+    this.exclude,
+    this.sourcePath,
+    this.classPath,
+    this.preamble,
+    this.importMap,
+    this.androidSdkConfig,
+    this.mavenDownloads,
+    this.summarizerOptions,
+    this.logLevel = Level.INFO,
+    this.dumpJsonTo,
+  });
+
+  /// Output configuration for generated bindings
+  OutputConfig outputConfig;
+
+  /// List of classes or packages for which bindings have to be generated.
+  ///
+  /// The names must be fully qualified, and it's assumed that the directory
+  /// structure corresponds to package naming. For example, com.abc.MyClass
+  /// should be resolvable as `com/abc/MyClass.java` from one of the provided
+  /// source paths. Same applies if ASM backend is used, except that the file
+  /// name suffix is `.class`.
+  List<String> classes;
+
+  /// Methods and fields to be excluded from generated bindings.
+  final BindingExclusions? exclude;
+
+  /// Paths to search for java source files.
+  ///
+  /// If a source package is downloaded through [mavenDownloads] option,
+  /// the corresponding source folder is automatically added and does not
+  /// need to be explicitly specified.
+  List<Uri>? sourcePath;
+
+  /// class path for scanning java libraries. If [backend] is `asm`, the
+  /// specified classpath is used to search for [classes], otherwise it's
+  /// merely used by the doclet API to find transitively referenced classes,
+  /// but not the specified classes / packages themselves.
+  List<Uri>? classPath;
+
+  /// Common text to be pasted on top of generated C and Dart files.
+  final String? preamble;
+
+  /// Additional java package -> dart package mappings (Experimental).
+  ///
+  /// a mapping com.abc.package -> 'package:my_package.dart/my_import.dart'
+  /// in this configuration suggests that any reference to a type from
+  /// com.abc.package shall resolve to an import of 'package:my_package.dart'.
+  ///
+  /// This can be as granular
+  /// `com.abc.package.Class -> 'package:abc/abc.dart'`
+  /// or coarse
+  /// `com.abc.package` -> 'package:abc/abc.dart'`
+  final Map<String, String>? importMap;
+
+  /// Configuration to search for Android SDK libraries (Experimental).
+  final AndroidSdkConfig? androidSdkConfig;
+
+  /// Configuration for auto-downloading JAR / source packages using maven,
+  /// along with their transitive dependencies.
+  final MavenDownloads? mavenDownloads;
+
+  /// Additional options for the summarizer component
+  final SummarizerOptions? summarizerOptions;
+
+  /// Directory containing the YAML configuration file, if any.
+  Uri? get configRoot => _configRoot;
+  Uri? _configRoot;
+
+  /// Log verbosity. The possible values in decreasing order of verbosity
+  /// are verbose > debug > info > warning > error. Defaults to [LogLevel.info]
+  Level logLevel = Level.INFO;
+
+  /// File to which JSON summary is written before binding generation.
+  final String? dumpJsonTo;
+
+  static final _levels = Map.fromEntries(
+      Level.LEVELS.map((l) => MapEntry(l.name.toLowerCase(), l)));
+  static List<Uri>? _toUris(List<String>? paths) =>
+      paths?.map(Uri.file).toList();
+
+  static Config parseArgs(List<String> args) {
+    final prov = YamlReader.parseArgs(args);
+
+    final List<String> missingValues = [];
+
+    T must<T>(T? Function(String) f, T ifNull, String property) {
+      final res = f(property);
+      if (res == null) {
+        missingValues.add(property);
+        return ifNull;
+      }
+      return res;
+    }
+
+    Uri? directoryUri(String? path) =>
+        path != null ? Uri.directory(path) : null;
+
+    MemberFilter<T>? regexFilter<T extends ClassMember>(String property) {
+      final exclusions = prov.getStringList(property);
+      if (exclusions == null) return null;
+      final List<MemberFilter<T>> filters = [];
+      for (var exclusion in exclusions) {
+        final split = exclusion.split('#');
+        if (split.length != 2) {
+          throw ConfigException('Error parsing exclusion: "$exclusion": '
+              'expected to be in binaryName#member format.');
+        }
+        filters.add(MemberNameFilter<T>.exclude(
+          RegExp(split[0]),
+          RegExp(split[1]),
+        ));
+      }
+      return CombinedMemberFilter<T>(filters);
+    }
+
+    String? getSdkRoot() {
+      final root = prov.getString(_Props.androidSdkRoot) ??
+          Platform.environment['ANDROID_SDK_ROOT'];
+      return root;
+    }
+
+    Level logLevelFromString(String? levelName) {
+      if (levelName == null) return Level.INFO;
+      final level = _levels[levelName.toLowerCase()];
+      if (level == null) {
+        throw ConfigException('Not a valid logging level: $levelName');
+      }
+      return level;
+    }
+
+    final configRoot = prov.getConfigRoot();
+    String resolveFromConfigRoot(String reference) =>
+        configRoot?.resolve(reference).toFilePath() ?? reference;
+
+    final config = Config(
+      sourcePath: _toUris(prov.getPathList(_Props.sourcePath)),
+      classPath: _toUris(prov.getPathList(_Props.classPath)),
+      classes: must(prov.getStringList, [], _Props.classes),
+      summarizerOptions: SummarizerOptions(
+        extraArgs: prov.getStringList(_Props.summarizerArgs) ?? const [],
+        backend: prov.getString(_Props.backend),
+        workingDirectory:
+            directoryUri(prov.getPath(_Props.summarizerWorkingDir)),
+      ),
+      exclude: BindingExclusions(
+        methods: regexFilter<Method>(_Props.excludeMethods),
+        fields: regexFilter<Field>(_Props.excludeFields),
+      ),
+      outputConfig: OutputConfig(
+        bindingsType: getBindingsType(
+          prov.getString(_Props.bindingsType),
+          BindingsType.cBased,
+        ),
+        cConfig: prov.hasValue(_Props.cCodeOutputConfig)
+            ? CCodeOutputConfig(
+                libraryName: must(prov.getString, '', _Props.libraryName),
+                path: Uri.file(must(prov.getPath, '.', _Props.cRoot)),
+                subdir: prov.getString(_Props.cSubdir),
+              )
+            : null,
+        dartConfig: DartCodeOutputConfig(
+          path: Uri.file(must(prov.getPath, '.', _Props.dartRoot)),
+          structure: getOutputStructure(
+            prov.getString(_Props.outputStructure),
+            OutputStructure.packageStructure,
+          ),
+        ),
+      ),
+      preamble: prov.getString(_Props.preamble),
+      importMap: prov.getStringMap(_Props.importMap),
+      mavenDownloads: prov.hasValue(_Props.mavenDownloads)
+          ? MavenDownloads(
+              sourceDeps: prov.getStringList(_Props.sourceDeps) ?? const [],
+              sourceDir: prov.getPath(_Props.mavenSourceDir) ??
+                  resolveFromConfigRoot(MavenDownloads.defaultMavenSourceDir),
+              jarOnlyDeps: prov.getStringList(_Props.jarOnlyDeps) ?? const [],
+              jarDir: prov.getPath(_Props.mavenJarDir) ??
+                  resolveFromConfigRoot(MavenDownloads.defaultMavenJarDir),
+            )
+          : null,
+      androidSdkConfig: prov.hasValue(_Props.androidSdkConfig)
+          ? AndroidSdkConfig(
+              versions: prov
+                  .getStringList(_Props.androidSdkVersions)
+                  ?.map(int.parse)
+                  .toList(),
+              sdkRoot: getSdkRoot(),
+              addGradleDeps: prov.getBool(_Props.addGradleDeps) ?? false,
+              // Leaving this as getString instead of getPath, because
+              // it's resolved later in android_sdk_tools.
+              androidExample: prov.getString(_Props.androidExample),
+            )
+          : null,
+      logLevel: logLevelFromString(
+        prov.getOneOf(
+          _Props.logLevel,
+          _levels.keys.toSet(),
+        ),
+      ),
+    );
+    if (missingValues.isNotEmpty) {
+      stderr.write('Following config values are required but not provided\n'
+          'Please provide these properties through YAML '
+          'or use the command line switch -D<property_name>=<value>.\n');
+      for (var missing in missingValues) {
+        stderr.writeln('* $missing');
+      }
+      if (missingValues.contains(_Props.androidSdkRoot)) {
+        stderr.writeln('Please specify ${_Props.androidSdkRoot} through '
+            'command line or ensure that the ANDROID_SDK_ROOT environment '
+            'variable is set.');
+      }
+      exit(1);
+    }
+    config._configRoot = configRoot;
+    return config;
+  }
+}
+
+class _Props {
+  static const summarizer = 'summarizer';
+  static const summarizerArgs = '$summarizer.extra_args';
+  static const summarizerWorkingDir = '$summarizer.working_dir';
+  static const backend = '$summarizer.backend';
+
+  static const sourcePath = 'source_path';
+  static const classPath = 'class_path';
+  static const classes = 'classes';
+  static const exclude = 'exclude';
+  static const excludeMethods = '$exclude.methods';
+  static const excludeFields = '$exclude.fields';
+
+  static const importMap = 'import_map';
+  static const outputConfig = 'output';
+  static const bindingsType = '$outputConfig.bindings_type';
+  static const cCodeOutputConfig = '$outputConfig.c';
+  static const dartCodeOutputConfig = '$outputConfig.dart';
+  static const cRoot = '$cCodeOutputConfig.path';
+  static const cSubdir = '$cCodeOutputConfig.subdir';
+  static const dartRoot = '$dartCodeOutputConfig.path';
+  static const outputStructure = '$dartCodeOutputConfig.structure';
+  static const libraryName = '$cCodeOutputConfig.library_name';
+  static const preamble = 'preamble';
+  static const logLevel = 'log_level';
+
+  static const mavenDownloads = 'maven_downloads';
+  static const sourceDeps = '$mavenDownloads.source_deps';
+  static const mavenSourceDir = '$mavenDownloads.source_dir';
+  static const jarOnlyDeps = '$mavenDownloads.jar_only_deps';
+  static const mavenJarDir = '$mavenDownloads.jar_dir';
+
+  static const androidSdkConfig = 'android_sdk_config';
+  static const androidSdkRoot = '$androidSdkConfig.sdk_root';
+  static const androidSdkVersions = '$androidSdkConfig.versions';
+  static const addGradleDeps = '$androidSdkConfig.add_gradle_deps';
+  static const androidExample = '$androidSdkConfig.android_example';
+}
diff --git a/pkgs/jnigen/lib/src/config/yaml_reader.dart b/pkgs/jnigen/lib/src/config/yaml_reader.dart
index 8f3aa67..4347ecb 100644
--- a/pkgs/jnigen/lib/src/config/yaml_reader.dart
+++ b/pkgs/jnigen/lib/src/config/yaml_reader.dart
@@ -7,20 +7,14 @@
 import 'package:args/args.dart';
 import 'package:yaml/yaml.dart';
 
-class ConfigError extends Error {
-  ConfigError(this.message);
-  final String message;
-  @override
-  String toString() => message;
-}
+import 'config_exception.dart';
 
 /// YAML Reader which enables to override specific values from command line.
 class YamlReader {
-  YamlReader.of(this.cli, this.yaml);
-  YamlReader.fromYaml(this.yaml) : cli = const {};
-  YamlReader.fromMap(this.cli) : yaml = const {};
+  YamlReader.of(this.cli, this.yaml, this.yamlFile);
   Map<String, String> cli;
   Map<dynamic, dynamic> yaml;
+  File? yamlFile;
 
   /// Parses the provided command line arguments and returns a [YamlReader].
   ///
@@ -57,7 +51,7 @@
         if (yamlInput is Map) {
           yamlMap = yamlInput;
         } else {
-          throw ConfigError('YAML config must be set of key value pairs');
+          throw ConfigException('YAML config must be set of key value pairs');
         }
       } on Exception catch (e) {
         stderr.writeln('cannot read $configFile: $e');
@@ -72,10 +66,11 @@
         final propertyValue = match.group(2);
         properties[propertyName!] = propertyValue!;
       } else {
-        throw ConfigError('override does not match expected pattern');
+        throw ConfigException('override does not match expected pattern');
       }
     }
-    return YamlReader.of(properties, yamlMap);
+    return YamlReader.of(
+        properties, yamlMap, configFile != null ? File(configFile) : null);
   }
 
   bool? getBool(String property) {
@@ -87,7 +82,7 @@
       if (v == 'false') {
         return false;
       }
-      throw ConfigError('expected boolean value for $property, got $v');
+      throw ConfigException('expected boolean value for $property, got $v');
     }
     return getYamlValue<bool>(property);
   }
@@ -97,18 +92,43 @@
     return configValue;
   }
 
+  /// Same as [getString] but path is resolved relative to YAML config if it's
+  /// from YAML config.
+  String? getPath(String property) {
+    final cliOverride = cli[property];
+    if (cliOverride != null) return cliOverride;
+    final path = getYamlValue<String>(property);
+    if (path == null) return null;
+    // In (very unlikely) case YAML config didn't come from a file,
+    // do not try to resolve anything.
+    if (yamlFile == null) return path;
+    final yamlDir = yamlFile!.parent;
+    return yamlDir.uri.resolve(path).toFilePath();
+  }
+
   List<String>? getStringList(String property) {
     final configValue = cli[property]?.split(';') ??
         getYamlValue<YamlList>(property)?.cast<String>();
     return configValue;
   }
 
+  List<String>? getPathList(String property) {
+    final cliOverride = cli[property]?.split(';');
+    if (cliOverride != null) return cliOverride;
+    final paths = getYamlValue<YamlList>(property)?.cast<String>();
+    if (paths == null) return null;
+    // In (very unlikely) case YAML config didn't come from a file.
+    if (yamlFile == null) return paths;
+    final yamlDir = yamlFile!.parent;
+    return paths.map((path) => yamlDir.uri.resolve(path).toFilePath()).toList();
+  }
+
   String? getOneOf(String property, Set<String> values) {
     final value = cli[property] ?? getYamlValue<String>(property);
     if (value == null || values.contains(value)) {
       return value;
     }
-    throw ConfigError('expected one of $values for $property');
+    throw ConfigException('expected one of $values for $property');
   }
 
   Map<String, String>? getStringMap(String property) {
@@ -126,7 +146,7 @@
       if (cursor is YamlMap || cursor is Map) {
         cursor = cursor[i];
       } else {
-        throw ConfigError('expected $current to be a YAML map');
+        throw ConfigException('expected $current to be a YAML map');
       }
       current = [if (current != '') current, i].join('.');
       if (cursor == null) {
@@ -134,8 +154,12 @@
       }
     }
     if (cursor is! T) {
-      throw ConfigError('expected $T for $property, got ${cursor.runtimeType}');
+      throw ConfigException(
+          'expected $T for $property, got ${cursor.runtimeType}');
     }
     return cursor;
   }
+
+  /// Returns URI of the directory containing YAML config.
+  Uri? getConfigRoot() => yamlFile?.parent.uri;
 }
diff --git a/pkgs/jnigen/lib/src/elements/elements.dart b/pkgs/jnigen/lib/src/elements/elements.dart
index 1d0581e..0c07b43 100644
--- a/pkgs/jnigen/lib/src/elements/elements.dart
+++ b/pkgs/jnigen/lib/src/elements/elements.dart
@@ -24,7 +24,7 @@
 // JSON. this allows us to reduce JSON size by providing Include.NON_NULL
 // option in java.
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class ClassDecl {
   /// Methods & properties already defined by dart JObject base class.
   static const Map<String, int> _definedSyms = {
@@ -90,7 +90,6 @@
 
   factory ClassDecl.fromJson(Map<String, dynamic> json) =>
       _$ClassDeclFromJson(json);
-  Map<String, dynamic> toJson() => _$ClassDeclToJson(this);
 
   String get internalName => binaryName.replaceAll(".", "/");
 
@@ -141,7 +140,7 @@
   array,
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class TypeUsage {
   TypeUsage({
     required this.shorthand,
@@ -182,14 +181,13 @@
     }
     return t;
   }
-  Map<String, dynamic> toJson() => _$TypeUsageToJson(this);
 }
 
 abstract class ReferredType {
   String get name;
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class PrimitiveType implements ReferredType {
   PrimitiveType({required this.name});
 
@@ -198,10 +196,9 @@
 
   factory PrimitiveType.fromJson(Map<String, dynamic> json) =>
       _$PrimitiveTypeFromJson(json);
-  Map<String, dynamic> toJson() => _$PrimitiveTypeToJson(this);
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class DeclaredType implements ReferredType {
   DeclaredType({
     required this.binaryName,
@@ -216,10 +213,9 @@
 
   factory DeclaredType.fromJson(Map<String, dynamic> json) =>
       _$DeclaredTypeFromJson(json);
-  Map<String, dynamic> toJson() => _$DeclaredTypeToJson(this);
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class TypeVar implements ReferredType {
   TypeVar({required this.name});
   @override
@@ -227,10 +223,9 @@
 
   factory TypeVar.fromJson(Map<String, dynamic> json) =>
       _$TypeVarFromJson(json);
-  Map<String, dynamic> toJson() => _$TypeVarToJson(this);
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class Wildcard implements ReferredType {
   Wildcard({this.extendsBound, this.superBound});
   TypeUsage? extendsBound, superBound;
@@ -240,10 +235,9 @@
 
   factory Wildcard.fromJson(Map<String, dynamic> json) =>
       _$WildcardFromJson(json);
-  Map<String, dynamic> toJson() => _$WildcardToJson(this);
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class ArrayType implements ReferredType {
   ArrayType({required this.type});
   TypeUsage type;
@@ -253,14 +247,13 @@
 
   factory ArrayType.fromJson(Map<String, dynamic> json) =>
       _$ArrayTypeFromJson(json);
-  Map<String, dynamic> toJson() => _$ArrayTypeToJson(this);
 }
 
 abstract class ClassMember {
   String get name;
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class Method implements ClassMember {
   Method(
       {this.annotations = const [],
@@ -297,10 +290,9 @@
   }
 
   factory Method.fromJson(Map<String, dynamic> json) => _$MethodFromJson(json);
-  Map<String, dynamic> toJson() => _$MethodToJson(this);
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class Param {
   Param(
       {this.annotations = const [],
@@ -314,10 +306,9 @@
   TypeUsage type;
 
   factory Param.fromJson(Map<String, dynamic> json) => _$ParamFromJson(json);
-  Map<String, dynamic> toJson() => _$ParamToJson(this);
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class Field implements ClassMember {
   Field(
       {this.annotations = const [],
@@ -344,10 +335,9 @@
   bool isIncluded = true;
 
   factory Field.fromJson(Map<String, dynamic> json) => _$FieldFromJson(json);
-  Map<String, dynamic> toJson() => _$FieldToJson(this);
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class TypeParam {
   TypeParam({required this.name, this.bounds = const []});
   String name;
@@ -358,10 +348,9 @@
 
   factory TypeParam.fromJson(Map<String, dynamic> json) =>
       _$TypeParamFromJson(json);
-  Map<String, dynamic> toJson() => _$TypeParamToJson(this);
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class JavaDocComment {
   JavaDocComment({String? comment}) : comment = comment ?? '';
   String comment;
@@ -371,10 +360,9 @@
 
   factory JavaDocComment.fromJson(Map<String, dynamic> json) =>
       _$JavaDocCommentFromJson(json);
-  Map<String, dynamic> toJson() => _$JavaDocCommentToJson(this);
 }
 
-@JsonSerializable(explicitToJson: true)
+@JsonSerializable(createToJson: false)
 class Annotation {
   Annotation(
       {required this.simpleName,
@@ -386,5 +374,4 @@
 
   factory Annotation.fromJson(Map<String, dynamic> json) =>
       _$AnnotationFromJson(json);
-  Map<String, dynamic> toJson() => _$AnnotationToJson(this);
 }
diff --git a/pkgs/jnigen/lib/src/elements/elements.g.dart b/pkgs/jnigen/lib/src/elements/elements.g.dart
index e59d1bc..68f91d6 100644
--- a/pkgs/jnigen/lib/src/elements/elements.g.dart
+++ b/pkgs/jnigen/lib/src/elements/elements.g.dart
@@ -20,8 +20,8 @@
           const {},
       simpleName: json['simpleName'] as String,
       binaryName: json['binaryName'] as String,
-      parentName: json['parentName'] as String?,
       packageName: json['packageName'] as String,
+      parentName: json['parentName'] as String?,
       typeParams: (json['typeParams'] as List<dynamic>?)
               ?.map((e) => TypeParam.fromJson(e as Map<String, dynamic>))
               .toList() ??
@@ -47,36 +47,12 @@
           (json['values'] as List<dynamic>?)?.map((e) => e as String).toList(),
     );
 
-Map<String, dynamic> _$ClassDeclToJson(ClassDecl instance) => <String, dynamic>{
-      'annotations': instance.annotations.map((e) => e.toJson()).toList(),
-      'javadoc': instance.javadoc?.toJson(),
-      'modifiers': instance.modifiers.toList(),
-      'simpleName': instance.simpleName,
-      'binaryName': instance.binaryName,
-      'parentName': instance.parentName,
-      'packageName': instance.packageName,
-      'typeParams': instance.typeParams.map((e) => e.toJson()).toList(),
-      'methods': instance.methods.map((e) => e.toJson()).toList(),
-      'fields': instance.fields.map((e) => e.toJson()).toList(),
-      'superclass': instance.superclass?.toJson(),
-      'interfaces': instance.interfaces.map((e) => e.toJson()).toList(),
-      'hasStaticInit': instance.hasStaticInit,
-      'hasInstanceInit': instance.hasInstanceInit,
-      'values': instance.values,
-    };
-
 TypeUsage _$TypeUsageFromJson(Map<String, dynamic> json) => TypeUsage(
       shorthand: json['shorthand'] as String,
       kind: $enumDecode(_$KindEnumMap, json['kind']),
       typeJson: json['type'] as Map<String, dynamic>,
     );
 
-Map<String, dynamic> _$TypeUsageToJson(TypeUsage instance) => <String, dynamic>{
-      'shorthand': instance.shorthand,
-      'kind': _$KindEnumMap[instance.kind]!,
-      'type': instance.typeJson,
-    };
-
 const _$KindEnumMap = {
   Kind.primitive: 'PRIMITIVE',
   Kind.typeVariable: 'TYPE_VARIABLE',
@@ -90,11 +66,6 @@
       name: json['name'] as String,
     );
 
-Map<String, dynamic> _$PrimitiveTypeToJson(PrimitiveType instance) =>
-    <String, dynamic>{
-      'name': instance.name,
-    };
-
 DeclaredType _$DeclaredTypeFromJson(Map<String, dynamic> json) => DeclaredType(
       binaryName: json['binaryName'] as String,
       simpleName: json['simpleName'] as String,
@@ -104,21 +75,10 @@
           const [],
     );
 
-Map<String, dynamic> _$DeclaredTypeToJson(DeclaredType instance) =>
-    <String, dynamic>{
-      'binaryName': instance.binaryName,
-      'simpleName': instance.simpleName,
-      'params': instance.params.map((e) => e.toJson()).toList(),
-    };
-
 TypeVar _$TypeVarFromJson(Map<String, dynamic> json) => TypeVar(
       name: json['name'] as String,
     );
 
-Map<String, dynamic> _$TypeVarToJson(TypeVar instance) => <String, dynamic>{
-      'name': instance.name,
-    };
-
 Wildcard _$WildcardFromJson(Map<String, dynamic> json) => Wildcard(
       extendsBound: json['extendsBound'] == null
           ? null
@@ -128,19 +88,10 @@
           : TypeUsage.fromJson(json['superBound'] as Map<String, dynamic>),
     );
 
-Map<String, dynamic> _$WildcardToJson(Wildcard instance) => <String, dynamic>{
-      'extendsBound': instance.extendsBound?.toJson(),
-      'superBound': instance.superBound?.toJson(),
-    };
-
 ArrayType _$ArrayTypeFromJson(Map<String, dynamic> json) => ArrayType(
       type: TypeUsage.fromJson(json['type'] as Map<String, dynamic>),
     );
 
-Map<String, dynamic> _$ArrayTypeToJson(ArrayType instance) => <String, dynamic>{
-      'type': instance.type.toJson(),
-    };
-
 Method _$MethodFromJson(Map<String, dynamic> json) => Method(
       annotations: (json['annotations'] as List<dynamic>?)
               ?.map((e) => Annotation.fromJson(e as Map<String, dynamic>))
@@ -166,16 +117,6 @@
           TypeUsage.fromJson(json['returnType'] as Map<String, dynamic>),
     );
 
-Map<String, dynamic> _$MethodToJson(Method instance) => <String, dynamic>{
-      'annotations': instance.annotations.map((e) => e.toJson()).toList(),
-      'javadoc': instance.javadoc?.toJson(),
-      'modifiers': instance.modifiers.toList(),
-      'name': instance.name,
-      'typeParams': instance.typeParams.map((e) => e.toJson()).toList(),
-      'params': instance.params.map((e) => e.toJson()).toList(),
-      'returnType': instance.returnType.toJson(),
-    };
-
 Param _$ParamFromJson(Map<String, dynamic> json) => Param(
       annotations: (json['annotations'] as List<dynamic>?)
               ?.map((e) => Annotation.fromJson(e as Map<String, dynamic>))
@@ -188,13 +129,6 @@
       type: TypeUsage.fromJson(json['type'] as Map<String, dynamic>),
     );
 
-Map<String, dynamic> _$ParamToJson(Param instance) => <String, dynamic>{
-      'annotations': instance.annotations.map((e) => e.toJson()).toList(),
-      'javadoc': instance.javadoc?.toJson(),
-      'name': instance.name,
-      'type': instance.type.toJson(),
-    };
-
 Field _$FieldFromJson(Map<String, dynamic> json) => Field(
       annotations: (json['annotations'] as List<dynamic>?)
               ?.map((e) => Annotation.fromJson(e as Map<String, dynamic>))
@@ -212,15 +146,6 @@
       defaultValue: json['defaultValue'],
     );
 
-Map<String, dynamic> _$FieldToJson(Field instance) => <String, dynamic>{
-      'annotations': instance.annotations.map((e) => e.toJson()).toList(),
-      'javadoc': instance.javadoc?.toJson(),
-      'modifiers': instance.modifiers.toList(),
-      'name': instance.name,
-      'type': instance.type.toJson(),
-      'defaultValue': instance.defaultValue,
-    };
-
 TypeParam _$TypeParamFromJson(Map<String, dynamic> json) => TypeParam(
       name: json['name'] as String,
       bounds: (json['bounds'] as List<dynamic>?)
@@ -229,21 +154,11 @@
           const [],
     );
 
-Map<String, dynamic> _$TypeParamToJson(TypeParam instance) => <String, dynamic>{
-      'name': instance.name,
-      'bounds': instance.bounds.map((e) => e.toJson()).toList(),
-    };
-
 JavaDocComment _$JavaDocCommentFromJson(Map<String, dynamic> json) =>
     JavaDocComment(
       comment: json['comment'] as String?,
     );
 
-Map<String, dynamic> _$JavaDocCommentToJson(JavaDocComment instance) =>
-    <String, dynamic>{
-      'comment': instance.comment,
-    };
-
 Annotation _$AnnotationFromJson(Map<String, dynamic> json) => Annotation(
       simpleName: json['simpleName'] as String,
       binaryName: json['binaryName'] as String,
@@ -252,10 +167,3 @@
           ) ??
           const {},
     );
-
-Map<String, dynamic> _$AnnotationToJson(Annotation instance) =>
-    <String, dynamic>{
-      'simpleName': instance.simpleName,
-      'binaryName': instance.binaryName,
-      'properties': instance.properties,
-    };
diff --git a/pkgs/jnigen/lib/src/generate_bindings.dart b/pkgs/jnigen/lib/src/generate_bindings.dart
index e372d45..b70cf8c 100644
--- a/pkgs/jnigen/lib/src/generate_bindings.dart
+++ b/pkgs/jnigen/lib/src/generate_bindings.dart
@@ -51,23 +51,20 @@
   final androidConfig = config.androidSdkConfig;
   if (androidConfig != null && androidConfig.addGradleDeps) {
     final deps = AndroidSdkTools.getGradleClasspaths(
-        androidConfig.androidExample ?? '.');
+      configRoot: config.configRoot,
+      androidProject: androidConfig.androidExample ?? '.',
+    );
     extraJars.addAll(deps.map(Uri.file));
   }
   if (androidConfig != null && androidConfig.versions != null) {
     final versions = androidConfig.versions!;
+    final androidSdkRoot =
+        androidConfig.sdkRoot ?? AndroidSdkTools.getAndroidSdkRoot();
     final androidJar = await AndroidSdkTools.getAndroidJarPath(
-        sdkRoot: androidConfig.sdkRoot, versionOrder: versions);
+        sdkRoot: androidSdkRoot, versionOrder: versions);
     if (androidJar != null) {
       extraJars.add(Uri.directory(androidJar));
     }
-    if (androidConfig.includeSources) {
-      final androidSources = await AndroidSdkTools.getAndroidSourcesPath(
-          sdkRoot: androidConfig.sdkRoot, versionOrder: versions);
-      if (androidSources != null) {
-        extraSources.add(Uri.directory(androidSources));
-      }
-    }
   }
 
   summarizer.addSourcePaths(extraSources);
diff --git a/pkgs/jnigen/lib/src/logging/logging.dart b/pkgs/jnigen/lib/src/logging/logging.dart
index bf4bbe9..561b06b 100644
--- a/pkgs/jnigen/lib/src/logging/logging.dart
+++ b/pkgs/jnigen/lib/src/logging/logging.dart
@@ -2,6 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+// coverage:ignore-file
+
 import 'dart:io';
 import 'package:logging/logging.dart';
 
@@ -16,11 +18,9 @@
   return message;
 }
 
-Logger log = Logger('jnigen');
-
-/// Set logging level to [level] and initialize the logger.
-void setLoggingLevel(Level level) {
-  Logger.root.level = level;
+Logger log = () {
+  final jnigenLogger = Logger('jnigen');
+  Logger.root.level = Level.INFO;
   Logger.root.onRecord.listen((r) {
     var message = '(${r.loggerName}) ${r.level.name}: ${r.message}';
     if ((r.level == Level.SHOUT || r.level == Level.SEVERE)) {
@@ -30,6 +30,15 @@
     }
     stderr.writeln(message);
   });
+  return jnigenLogger;
+}();
+
+/// Set logging level to [level].
+void setLoggingLevel(Level level) {
+  /// This initializes `log` as a side effect, so that level setting we apply
+  /// is always the last one applied.
+  log.fine('Set log level: $level');
+  Logger.root.level = level;
 }
 
 /// Prints [message] without logging information.
@@ -40,7 +49,7 @@
 }
 
 extension FatalErrors on Logger {
-  void fatal(Object? message, {int exitCode = 2}) {
+  void fatal(Object? message, {int exitCode = 1}) {
     message = _colorize('Fatal: $message', _ansiRed);
     stderr.writeln(message);
     exit(exitCode);
diff --git a/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart b/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart
index e62d1e8..703db13 100644
--- a/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart
+++ b/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart
@@ -7,13 +7,34 @@
 
 import 'package:jnigen/src/logging/logging.dart';
 
+class _AndroidToolsException implements Exception {
+  _AndroidToolsException(this.message);
+  String message;
+  @override
+  String toString() => message;
+}
+
+class SdkNotFoundException extends _AndroidToolsException {
+  SdkNotFoundException(String message) : super(message);
+}
+
+class GradleException extends _AndroidToolsException {
+  GradleException(String message) : super(message);
+}
+
 class AndroidSdkTools {
-  /// get path for android API sources
-  static Future<String?> _getVersionDir(
-      String relative, String? sdkRoot, List<int> versionOrder) async {
-    if (sdkRoot == null) {
-      throw ArgumentError('SDK Root not provided');
+  static String getAndroidSdkRoot() {
+    final envVar = Platform.environment['ANDROID_SDK_ROOT'];
+    if (envVar == null) {
+      throw SdkNotFoundException('Android SDK not found. Please set '
+          'ANDROID_SDK_ROOT environment variable or specify through command '
+          'line override.');
     }
+    return envVar;
+  }
+
+  static Future<String?> _getVersionDir(
+      String relative, String sdkRoot, List<int> versionOrder) async {
     final parent = join(sdkRoot, relative);
     for (var version in versionOrder) {
       final dir = Directory(join(parent, 'android-$version'));
@@ -24,15 +45,8 @@
     return null;
   }
 
-  static Future<String?> getAndroidSourcesPath(
-      {String? sdkRoot, required List<int> versionOrder}) async {
-    final dir = await _getVersionDir('sources', sdkRoot, versionOrder);
-    log.info('Found sources at $dir');
-    return dir;
-  }
-
-  static Future<String?> _getFile(String relative, String file, String? sdkRoot,
-      List<int> versionOrder) async {
+  static Future<String?> _getFile(String sdkRoot, String relative,
+      List<int> versionOrder, String file) async {
     final platform = await _getVersionDir(relative, sdkRoot, versionOrder);
     if (platform == null) return null;
     final filePath = join(platform, file);
@@ -44,8 +58,8 @@
   }
 
   static Future<String?> getAndroidJarPath(
-          {String? sdkRoot, required List<int> versionOrder}) async =>
-      await _getFile('platforms', 'android.jar', sdkRoot, versionOrder);
+          {required String sdkRoot, required List<int> versionOrder}) async =>
+      await _getFile(sdkRoot, 'platforms', versionOrder, 'android.jar');
 
   static const _gradleListDepsFunction = '''
 task listDependencies(type: Copy) {
@@ -71,8 +85,12 @@
   ///
   /// If current project is not directly buildable by gradle, eg: a plugin,
   /// a relative path to other project can be specified using [androidProject].
-  static List<String> getGradleClasspaths([String androidProject = '.']) {
+  static List<String> getGradleClasspaths(
+      {Uri? configRoot, String androidProject = '.'}) {
     log.info('trying to obtain gradle classpaths...');
+    if (configRoot != null) {
+      androidProject = join(configRoot.toFilePath(), androidProject);
+    }
     final android = join(androidProject, 'android');
     final buildGradle = join(android, 'build.gradle');
     final buildGradleOld = join(android, 'build.gradle.old');
@@ -96,9 +114,10 @@
     if (procRes.exitCode != 0) {
       final inAndroidProject =
           (androidProject == '.') ? '' : ' in $androidProject';
-      throw Exception('\n\ngradle exited with exit code ${procRes.exitCode}\n'
-          'This can be related to a known issue with gradle. Please run '
-          '`flutter build apk`$inAndroidProject and try again\n');
+      throw GradleException('\n\ngradle exited with exit code '
+          '${procRes.exitCode}\n. This can be related to a known issue with '
+          'gradle. Please run `flutter build apk`$inAndroidProject and try '
+          'again\n');
     }
     final classpaths = (procRes.stdout as String)
         .trim()
diff --git a/pkgs/jnigen/lib/src/tools/build_summarizer.dart b/pkgs/jnigen/lib/src/tools/build_summarizer.dart
index d2dd22d..38e395e 100644
--- a/pkgs/jnigen/lib/src/tools/build_summarizer.dart
+++ b/pkgs/jnigen/lib/src/tools/build_summarizer.dart
@@ -2,6 +2,13 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+// TODO(#43): Address concurrently building summarizer.
+//
+// In the current state summarizer has to be built before tests, which run
+// concurrently. Ignoring coverage for this file until that issue is addressed.
+//
+// coverage:ignore-file
+
 import 'dart:io';
 
 import 'package:path/path.dart';
diff --git a/pkgs/jnigen/lib/src/tools/maven_tools.dart b/pkgs/jnigen/lib/src/tools/maven_tools.dart
index 8d1ccd9..07ce764 100644
--- a/pkgs/jnigen/lib/src/tools/maven_tools.dart
+++ b/pkgs/jnigen/lib/src/tools/maven_tools.dart
@@ -75,24 +75,6 @@
     await tempDir.delete(recursive: true);
   }
 
-  /// Get classpath string using JARs in maven's local repository.
-  static Future<String> getMavenClassPath(List<MavenDependency> deps) async {
-    final tempDir = await currentDir.createTemp("maven_temp_");
-    final tempClassPath = join(tempDir.path, "maven_classpath");
-    await _runMavenCommand(
-        deps,
-        [
-          'dependency:build-classpath',
-          '-Dmdep.outputFile=$tempClassPath',
-        ],
-        tempDir);
-    final classPathFile = File(tempClassPath);
-    final classpath = await classPathFile.readAsString();
-    await classPathFile.delete();
-    await tempDir.delete(recursive: true);
-    return classpath;
-  }
-
   static String _getStubPom(List<MavenDependency> deps,
       {String javaVersion = '11'}) {
     final depDecls = <String>[];
diff --git a/pkgs/jnigen/lib/src/util/command_output.dart b/pkgs/jnigen/lib/src/util/command_output.dart
index 64deac2..1e7a9e0 100644
--- a/pkgs/jnigen/lib/src/util/command_output.dart
+++ b/pkgs/jnigen/lib/src/util/command_output.dart
@@ -4,13 +4,5 @@
 
 import 'dart:convert';
 
-Stream<String> commandOutputStream(
-        String Function(String) lineMapper, Stream<List<int>> input) =>
-    input.transform(Utf8Decoder()).transform(LineSplitter()).map(lineMapper);
-
-Stream<String> prefixedCommandOutputStream(
-        String prefix, Stream<List<int>> input) =>
-    commandOutputStream((line) => '$prefix $line', input);
-
 void collectOutputStream(Stream<List<int>> stream, StringBuffer buffer) =>
     stream.transform(Utf8Decoder()).forEach(buffer.write);
diff --git a/pkgs/jnigen/lib/src/util/find_package.dart b/pkgs/jnigen/lib/src/util/find_package.dart
index 5c1650c..89d95dd 100644
--- a/pkgs/jnigen/lib/src/util/find_package.dart
+++ b/pkgs/jnigen/lib/src/util/find_package.dart
@@ -44,5 +44,3 @@
   }
   return false;
 }
-
-Future<Uri?> findPackageJni() => findPackageRoot('jni');
diff --git a/pkgs/jnigen/pubspec.yaml b/pkgs/jnigen/pubspec.yaml
index 34401ce..54e2b36 100644
--- a/pkgs/jnigen/pubspec.yaml
+++ b/pkgs/jnigen/pubspec.yaml
@@ -11,7 +11,7 @@
   sdk: '>=2.17.0 <3.0.0'
 
 dependencies:
-  json_annotation: ^4.6.0
+  json_annotation: ^4.7.0
   package_config: ^2.1.0
   path: ^1.8.0
   args: ^2.3.0
@@ -24,5 +24,5 @@
     path: ../jni
   test: ^1.17.5
   build_runner: ^2.2.0
-  json_serializable: ^6.3.1
+  json_serializable: ^6.5.4
 
diff --git a/pkgs/jnigen/test/config_test.dart b/pkgs/jnigen/test/config_test.dart
index 3dcff1c..883bae8 100644
--- a/pkgs/jnigen/test/config_test.dart
+++ b/pkgs/jnigen/test/config_test.dart
@@ -52,8 +52,6 @@
     expect(ba, isNotNull, reason: "androidSdkConfig");
     expect(aa.versions, ba!.versions, reason: "androidSdkConfig.versions");
     expect(aa.sdkRoot, ba.sdkRoot, reason: "androidSdkConfig.sdkRoot");
-    expect(aa.includeSources, ba.includeSources,
-        reason: "androidSdkConfig.includeSources");
   } else {
     expect(ba, isNull, reason: "androidSdkConfig");
   }
@@ -71,10 +69,32 @@
   }
 }
 
+final jnigenYaml = join(jacksonCoreTests, 'jnigen.yaml');
+
+Config parseYamlConfig({List<String> overrides = const []}) =>
+    Config.parseArgs(['--config', jnigenYaml, ...overrides]);
+
+void testForErrorChecking(
+    {required String name,
+    required List<String> overrides,
+    dynamic Function(Config)? function}) {
+  test(name, () {
+    expect(
+      () {
+        final config = parseYamlConfig(overrides: overrides);
+        if (function != null) {
+          function(config);
+        }
+      },
+      throwsA(isA<ConfigException>()),
+    );
+  });
+}
+
 void main() {
   final config = Config.parseArgs([
     '--config',
-    join(jacksonCoreTests, 'jnigen.yaml'),
+    jnigenYaml,
     '-Doutput.c.path=$testSrc/',
     '-Doutput.dart.path=$testLib/',
   ]);
@@ -82,4 +102,23 @@
   test('compare configuration values', () {
     expectConfigsAreEqual(config, getConfig(root: join(thirdParty, 'test_')));
   });
+
+  group('Test for config error checking', () {
+    testForErrorChecking(
+      name: 'Invalid bindings type',
+      overrides: ['-Doutput.bindings_type=c_base'],
+    );
+    testForErrorChecking(
+      name: 'Invalid output structure',
+      overrides: ['-Doutput.dart.structure=singl_file'],
+    );
+    testForErrorChecking(
+      name: 'Dart path not ending with /',
+      overrides: ['-Doutput.dart.path=lib'],
+    );
+    testForErrorChecking(
+      name: 'Invalid log level',
+      overrides: ['-Dlog_level=inf'],
+    );
+  });
 }
diff --git a/pkgs/jnigen/test/jackson_core_test/jnigen.yaml b/pkgs/jnigen/test/jackson_core_test/jnigen.yaml
index 1cfeb56..54c90ba 100644
--- a/pkgs/jnigen/test/jackson_core_test/jnigen.yaml
+++ b/pkgs/jnigen/test/jackson_core_test/jnigen.yaml
@@ -1,13 +1,13 @@
 maven_downloads:
   source_deps:
     - 'com.fasterxml.jackson.core:jackson-core:2.13.4'
-  source_dir: test/jackson_core_test/third_party/java
-  jar_dir: test/jackson_core_test/third_party/jar
+  source_dir: third_party/java/
+  jar_dir: third_party/jar/
 
 output:
   bindings_type: dart_only
   dart:
-    path: test/jackson_core_test/third_party/lib/
+    path: third_party/lib/
 
 classes:
   - 'com.fasterxml.jackson.core.JsonFactory'
diff --git a/pkgs/jnigen/test/regenerate_examples_test.dart b/pkgs/jnigen/test/regenerate_examples_test.dart
new file mode 100644
index 0000000..27dcdce
--- /dev/null
+++ b/pkgs/jnigen/test/regenerate_examples_test.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:path/path.dart' hide equals;
+
+import 'package:jnigen/jnigen.dart';
+import 'package:jnigen/tools.dart';
+
+import 'test_util/test_util.dart';
+
+final inAppJava = join('example', 'in_app_java');
+final inAppJavaYaml = join(inAppJava, 'jnigen.yaml');
+final notificationPlugin = join('example', 'notification_plugin');
+final notificationPluginYaml = join(notificationPlugin, 'jnigen.yaml');
+
+/// Generates bindings using jnigen config in [exampleName] and compares
+/// them to provided reference outputs.
+///
+/// [dartOutput] and [cOutput] are relative paths from example project dir.
+void testExample(String exampleName, String dartOutput, String? cOutput) {
+  test('Generate and compare bindings for $exampleName', () async {
+    final examplePath = join('example', exampleName);
+    final configPath = join(examplePath, 'jnigen.yaml');
+
+    final dartBindingsPath = join(examplePath, dartOutput);
+    String? cBindingsPath;
+    if (cOutput != null) {
+      cBindingsPath = join(examplePath, cOutput);
+    }
+
+    final config = Config.parseArgs(['--config', configPath]);
+    try {
+      await generateAndCompareBindings(config, dartBindingsPath, cBindingsPath);
+    } on GradleException catch (_) {
+      stderr.writeln('Skip: $exampleName');
+    }
+  });
+}
+
+void main() {
+  testExample(
+    'in_app_java',
+    join('lib', 'android_utils.dart'),
+    join('src', 'android_utils'),
+  );
+  testExample('pdfbox_plugin', join('lib', 'src', 'third_party'), 'src');
+  testExample(
+    'notification_plugin',
+    join('lib', 'notifications.dart'),
+    'src',
+  );
+}
diff --git a/pkgs/jnigen/test/test_util/test_util.dart b/pkgs/jnigen/test/test_util/test_util.dart
index 27fa75b..df6b1b9 100644
--- a/pkgs/jnigen/test/test_util/test_util.dart
+++ b/pkgs/jnigen/test/test_util/test_util.dart
@@ -76,8 +76,15 @@
   await generateJniBindings(config);
 }
 
-Future<void> generateAndCompareBindings(
-    Config config, String dartPath, String cPath) async {
+/// Generates and compares bindings with reference bindings.
+///
+/// [dartReferenceBindings] can be directory or file depending on output
+/// configuration.
+///
+/// If the config generates C code, [cReferenceBindings] must be a non-null
+/// directory path.
+Future<void> generateAndCompareBindings(Config config,
+    String dartReferenceBindings, String? cReferenceBindings) async {
   final currentDir = Directory.current;
   final tempDir = currentDir.createTempSync("jnigen_test_temp");
   final tempSrc = tempDir.uri.resolve("src/");
@@ -88,9 +95,9 @@
       : tempDir.uri.resolve("lib/");
   try {
     await _generateTempBindings(config, tempDir);
-    comparePaths(dartPath, tempLib.toFilePath());
+    comparePaths(dartReferenceBindings, tempLib.toFilePath());
     if (config.outputConfig.bindingsType == BindingsType.cBased) {
-      comparePaths(cPath, tempSrc.toFilePath());
+      comparePaths(cReferenceBindings!, tempSrc.toFilePath());
     }
   } finally {
     tempDir.deleteSync(recursive: true);